fork of https://github.com/sourcegraph/zoekt
1package main
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "reflect"
15 "sort"
16 "strconv"
17 "strings"
18 "testing"
19 "time"
20
21 "github.com/sourcegraph/log/logtest"
22
23 "google.golang.org/grpc"
24 "google.golang.org/protobuf/testing/protocmp"
25 "google.golang.org/protobuf/types/known/timestamppb"
26
27 "github.com/google/go-cmp/cmp"
28 "github.com/google/go-cmp/cmp/cmpopts"
29
30 "github.com/sourcegraph/zoekt"
31 proto "github.com/sourcegraph/zoekt/cmd/zoekt-sourcegraph-indexserver/protos/sourcegraph/zoekt/configuration/v1"
32)
33
34func TestIterateIndexOptions_Fingerprint(t *testing.T) {
35 t.Run("gRPC", func(t *testing.T) {
36 fingerprintV0 := &proto.Fingerprint{
37 Identifier: 100,
38 GeneratedAt: timestamppb.New(time.Unix(100, 0)),
39 }
40
41 fingerprintV1 := &proto.Fingerprint{
42 Identifier: 101,
43 GeneratedAt: timestamppb.New(time.Unix(101, 0)),
44 }
45
46 fingerprintV2 := &proto.Fingerprint{
47 Identifier: 102,
48 GeneratedAt: timestamppb.New(time.Unix(102, 0)),
49 }
50
51 mkSearchConfigurationResponse := func(fingerprint *proto.Fingerprint, repoIDs ...int32) *proto.SearchConfigurationResponse {
52 repositories := make([]*proto.ZoektIndexOptions, 0, len(repoIDs))
53 for _, repoID := range repoIDs {
54 repositories = append(repositories, &proto.ZoektIndexOptions{
55 RepoId: repoID,
56 })
57 }
58
59 return &proto.SearchConfigurationResponse{
60 UpdatedOptions: repositories,
61 Fingerprint: fingerprint,
62 }
63 }
64
65 grpcClient := &mockGRPCClient{
66 mockList: func(_ context.Context, in *proto.ListRequest, opts ...grpc.CallOption) (*proto.ListResponse, error) {
67 return &proto.ListResponse{
68 RepoIds: []int32{1, 2, 3},
69 }, nil
70 },
71 }
72
73 clientOpts := []SourcegraphClientOption{
74 WithBatchSize(1),
75 WithShouldUseGRPC(true),
76 WithGRPCClient(grpcClient),
77 }
78
79 testURL := url.URL{Scheme: "http", Host: "does.not.matter", Path: "/"}
80 sg := newSourcegraphClient(&testURL, "", clientOpts...)
81
82 type step struct {
83 name string
84
85 wantFingerprint *proto.Fingerprint
86 returnFingerprint *proto.Fingerprint
87 returnErr error
88 skipCheckingRepoIDs bool
89 }
90
91 for _, step := range []step{
92 {
93 name: "first call",
94 wantFingerprint: nil,
95 returnFingerprint: fingerprintV0,
96 },
97 {
98 name: "second call (should provide fingerprint from last time)",
99 wantFingerprint: fingerprintV0,
100 returnFingerprint: fingerprintV1,
101 },
102 {
103 name: "error",
104 wantFingerprint: fingerprintV1,
105 returnFingerprint: fingerprintV2,
106
107 returnErr: fmt.Errorf("boom"),
108 skipCheckingRepoIDs: true, // don't bother checking repoIDs if we expect an error
109 },
110 {
111 name: "call after error (should ignore fingerprint from last time, and provide the older one)",
112 wantFingerprint: fingerprintV1,
113 returnFingerprint: fingerprintV2,
114 },
115 } {
116 t.Run(step.name, func(t *testing.T) {
117 called := false
118 grpcClient.mockSearchConfiguration = func(_ context.Context, in *proto.SearchConfigurationRequest, opts ...grpc.CallOption) (*proto.SearchConfigurationResponse, error) {
119 called = true
120
121 diff := cmp.Diff(step.wantFingerprint, in.GetFingerprint(), protocmp.Transform())
122 if diff != "" {
123 t.Fatalf("unexpected fingerprint (-want +got):\n%s", diff)
124 }
125
126 return mkSearchConfigurationResponse(step.returnFingerprint, in.RepoIds...), step.returnErr
127 }
128
129 result, err := sg.List(context.Background(), nil)
130 if err != nil {
131 t.Fatalf("unexpected error from List: %v", err)
132 }
133
134 var iteratedIDs []uint32
135 result.IterateIndexOptions(func(options IndexOptions) {
136 iteratedIDs = append(iteratedIDs, options.RepoID)
137 })
138
139 if !called {
140 t.Fatal("expected SearchConfiguration to be called")
141 }
142
143 if step.skipCheckingRepoIDs {
144 return
145 }
146
147 sort.Slice(iteratedIDs, func(i, j int) bool {
148 return iteratedIDs[i] < iteratedIDs[j]
149 })
150
151 expectedIDs := []uint32{1, 2, 3}
152 sort.Slice(expectedIDs, func(i, j int) bool {
153 return expectedIDs[i] < expectedIDs[j]
154 })
155
156 if diff := cmp.Diff(expectedIDs, iteratedIDs); diff != "" {
157 t.Fatalf("unexpected repo ids (-want +got):\n%s", diff)
158 }
159 })
160 }
161
162 })
163
164 t.Run("REST", func(t *testing.T) {
165 fingerprintV0 := "v0"
166 fingerprintV1 := "v1"
167 fingerprintV2 := "v2"
168
169 handleList := func(w http.ResponseWriter, _ *http.Request) {
170 data := struct {
171 RepoIDs []uint32
172 }{
173 RepoIDs: []uint32{1, 2, 3},
174 }
175
176 json.NewEncoder(w).Encode(data)
177 }
178
179 searchConfigurationHandler := func(w http.ResponseWriter, r *http.Request) {
180 http.Error(w, "this search configuration handler hasn't been overridden", http.StatusForbidden)
181 }
182
183 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
184 switch r.URL.Path {
185 case "/.internal/search/configuration":
186 searchConfigurationHandler(w, r)
187 case "/.internal/repos/index":
188 handleList(w, r)
189 default:
190 t.Fatalf("unexpected path: %s", r.URL.Path)
191 }
192 }))
193 defer server.Close()
194
195 clientOpts := []SourcegraphClientOption{
196 WithBatchSize(1),
197 WithShouldUseGRPC(false),
198 }
199
200 testURL, err := url.Parse(server.URL)
201 if err != nil {
202 t.Fatalf("unexpected error parsing URL: %v", err)
203 }
204
205 sg := newSourcegraphClient(testURL, "", clientOpts...)
206
207 type step struct {
208 name string
209
210 wantFingerprint string
211 returnFingerprint string
212 returnErr error
213 skipCheckingRepoIDs bool
214 }
215
216 for _, step := range []step{
217 {
218 name: "first call",
219 wantFingerprint: "",
220 returnFingerprint: fingerprintV0,
221 },
222 {
223 name: "second call (should provide fingerprint from last time)",
224 wantFingerprint: fingerprintV0,
225 returnFingerprint: fingerprintV1,
226 },
227 {
228 name: "error",
229 wantFingerprint: fingerprintV1,
230 returnFingerprint: fingerprintV2,
231
232 returnErr: fmt.Errorf("boom"),
233 skipCheckingRepoIDs: true, // don't bother checking repoIDs if we expect an error
234 },
235 {
236 name: "call after error (should ignore fingerprint from last time, and provide the older one)",
237 wantFingerprint: fingerprintV1,
238 returnFingerprint: fingerprintV2,
239 },
240 } {
241 t.Run(step.name, func(t *testing.T) {
242 called := false
243
244 searchConfigurationHandler = func(w http.ResponseWriter, r *http.Request) {
245 called = true
246
247 fingerprint := r.Header.Get(fingerprintHeader)
248 if diff := cmp.Diff(step.wantFingerprint, fingerprint); diff != "" {
249 t.Fatalf("unexpected fingerprint (-want +got):\n%s", diff)
250 }
251
252 w.Header().Set(fingerprintHeader, step.returnFingerprint)
253
254 if step.returnErr != nil {
255 // The status code is a bit of a hack, but it prevents
256 // the retry logic from kicking in and stalling the test for 45 seconds.
257 http.Error(w, step.returnErr.Error(), http.StatusBadRequest)
258 return
259 }
260
261 if err := r.ParseForm(); err != nil {
262 http.Error(w, fmt.Sprintf("unexpected error parsing form for repoIDs: %v", err.Error()), http.StatusBadRequest)
263 return
264 }
265
266 repoIDs := make([]uint32, 0, len(r.Form["repoID"]))
267 for _, idStr := range r.Form["repoID"] {
268 id, err := strconv.Atoi(idStr)
269 if err != nil {
270 http.Error(w, fmt.Sprintf("invalid repo id %s: %s", idStr, err), http.StatusBadRequest)
271 return
272 }
273 repoIDs = append(repoIDs, uint32(id))
274 }
275
276 optionJSONSlice := make([][]byte, 0, len(repoIDs))
277 for _, repoID := range repoIDs {
278 option := IndexOptions{
279 RepoID: repoID,
280 }
281
282 optionJSON, err := json.Marshal(option)
283 if err != nil {
284 t.Fatalf("unexpected error marshalling JSON: %v", err)
285 }
286
287 optionJSONSlice = append(optionJSONSlice, optionJSON)
288 }
289
290 w.Write(bytes.Join(optionJSONSlice, []byte("\n")))
291 }
292
293 result, err := sg.List(context.Background(), nil)
294 if err != nil {
295 t.Fatalf("unexpected error from List: %v", err)
296 }
297
298 var iteratedIDs []uint32
299 result.IterateIndexOptions(func(options IndexOptions) {
300 iteratedIDs = append(iteratedIDs, options.RepoID)
301 })
302
303 if !called {
304 t.Fatal("expected SearchConfiguration to be called")
305 }
306
307 if step.skipCheckingRepoIDs {
308 return
309 }
310
311 sort.Slice(iteratedIDs, func(i, j int) bool {
312 return iteratedIDs[i] < iteratedIDs[j]
313 })
314
315 expectedIDs := []uint32{1, 2, 3}
316 sort.Slice(expectedIDs, func(i, j int) bool {
317 return expectedIDs[i] < expectedIDs[j]
318 })
319
320 if diff := cmp.Diff(expectedIDs, iteratedIDs); diff != "" {
321 t.Fatalf("unexpected repo ids (-want +got):\n%s", diff)
322 }
323 })
324 }
325
326 })
327}
328
329func TestGetIndexOptions(t *testing.T) {
330 t.Run("gRPC", func(t *testing.T) {
331
332 type testCase struct {
333 name string
334 response *proto.SearchConfigurationResponse
335 want *IndexOptions
336 wantErr string
337 }
338
339 for _, tc := range []testCase{
340 {
341 name: "symbols, large files",
342 response: &proto.SearchConfigurationResponse{
343 UpdatedOptions: []*proto.ZoektIndexOptions{
344 {
345 Symbols: true,
346 LargeFiles: []string{"foo", "bar"},
347 },
348 },
349 },
350 want: &IndexOptions{
351 Symbols: true,
352 LargeFiles: []string{"foo", "bar"},
353 },
354 },
355 {
356 name: "no symbols , large files",
357 response: &proto.SearchConfigurationResponse{
358 UpdatedOptions: []*proto.ZoektIndexOptions{
359 {
360 Symbols: true,
361 LargeFiles: []string{"foo", "bar"},
362 },
363 },
364 },
365 want: &IndexOptions{
366 Symbols: true,
367 LargeFiles: []string{"foo", "bar"},
368 },
369 },
370
371 {
372 name: "empty",
373 response: nil,
374 want: nil,
375 },
376
377 {
378 name: "symbols",
379 response: &proto.SearchConfigurationResponse{
380 UpdatedOptions: []*proto.ZoektIndexOptions{
381 {
382 Symbols: true,
383 },
384 },
385 },
386 want: &IndexOptions{
387 Symbols: true,
388 },
389 },
390 {
391 name: "repoID",
392 response: &proto.SearchConfigurationResponse{
393 UpdatedOptions: []*proto.ZoektIndexOptions{
394 {
395 RepoId: 123,
396 },
397 },
398 },
399 want: &IndexOptions{
400 RepoID: 123,
401 },
402 },
403 {
404 name: "error",
405 response: &proto.SearchConfigurationResponse{
406 UpdatedOptions: []*proto.ZoektIndexOptions{
407 {
408 Error: "boom",
409 },
410 },
411 },
412 want: nil,
413 wantErr: "boom",
414 },
415 } {
416 called := false
417 mockClient := &mockGRPCClient{
418 mockSearchConfiguration: func(_ context.Context, _ *proto.SearchConfigurationRequest, _ ...grpc.CallOption) (*proto.SearchConfigurationResponse, error) {
419 called = true
420 return tc.response, nil
421 },
422 }
423
424 testURL := &url.URL{
425 Scheme: "http",
426 Host: "does.not.matter",
427 Path: "/",
428 }
429
430 sg := newSourcegraphClient(
431 testURL,
432 "",
433 WithShouldUseGRPC(true),
434 WithGRPCClient(mockClient),
435 )
436
437 var got IndexOptions
438 var err error
439 sg.ForceIterateIndexOptions(func(o IndexOptions) {
440 got = o
441 }, func(_ uint32, e error) {
442 err = e
443 }, 123)
444
445 if !called {
446 t.Fatal("expected mock to be called")
447 }
448
449 if err != nil {
450 if tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr) {
451 t.Fatalf("unexpected error: %v", err)
452 }
453 }
454
455 if tc.want == nil {
456 continue
457 }
458
459 tc.want.CloneURL = sg.getCloneURL(got.Name)
460
461 if diff := cmp.Diff(tc.want, &got, cmpopts.EquateEmpty()); diff != "" {
462 t.Errorf("mismatch (-want +got):\n%s", diff)
463 }
464 }
465
466 // Mimic our fingerprint API, which doesn't return anything if the
467 // repo hasn't changed.
468 t.Run("unchanged", func(t *testing.T) {
469
470 called := false
471 mockClient := &mockGRPCClient{
472 mockSearchConfiguration: func(_ context.Context, _ *proto.SearchConfigurationRequest, _ ...grpc.CallOption) (*proto.SearchConfigurationResponse, error) {
473 called = true
474 return nil, nil
475 },
476 }
477
478 testURL := &url.URL{
479 Scheme: "http",
480 Host: "does.not.matter",
481 Path: "/",
482 }
483
484 sg := newSourcegraphClient(
485 testURL,
486 "",
487 WithShouldUseGRPC(true),
488 WithGRPCClient(mockClient))
489
490 gotAtLeastOneOption := false
491 var err error
492 sg.ForceIterateIndexOptions(func(_ IndexOptions) {
493 gotAtLeastOneOption = true
494 }, func(_ uint32, e error) {
495 err = e
496 }, 123)
497
498 if !called {
499 t.Fatal("expected mock to be called")
500 }
501
502 if err != nil {
503 t.Fatalf("unexpected error: %v", err)
504 }
505
506 if gotAtLeastOneOption {
507 t.Fatalf("expected no options, got %v", gotAtLeastOneOption)
508 }
509 })
510
511 })
512 t.Run("REST", func(t *testing.T) {
513 var response []byte
514 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
515 if err := r.ParseForm(); err != nil {
516 http.Error(w, err.Error(), http.StatusBadRequest)
517 return
518 }
519 if got, want := r.URL.String(), "/.internal/search/configuration"; got != want {
520 http.Error(w, fmt.Sprintf("got URL %v want %v", got, want), http.StatusBadRequest)
521 return
522 }
523 if got, want := r.Form, (url.Values{"repoID": []string{"123"}}); !reflect.DeepEqual(got, want) {
524 http.Error(w, fmt.Sprintf("got URL %v want %v", got, want), http.StatusBadRequest)
525 return
526 }
527 _, _ = w.Write(response)
528 }))
529 defer server.Close()
530
531 u, err := url.Parse(server.URL)
532 if err != nil {
533 t.Fatal(err)
534 }
535
536 sg := newSourcegraphClient(u, "", WithBatchSize(0))
537
538 cases := map[string]*IndexOptions{
539 `{"Symbols": true, "LargeFiles": ["foo","bar"]}`: {
540 Symbols: true,
541 LargeFiles: []string{"foo", "bar"},
542 },
543
544 `{"Symbols": false, "LargeFiles": ["foo","bar"]}`: {
545 LargeFiles: []string{"foo", "bar"},
546 },
547
548 `{}`: {},
549
550 `{"Symbols": true}`: {
551 Symbols: true,
552 },
553
554 `{"RepoID": 123}`: {
555 RepoID: 123,
556 },
557
558 `{"Error": "boom"}`: nil,
559 }
560
561 for r, want := range cases {
562 response = []byte(r)
563
564 var got IndexOptions
565 var err error
566 sg.ForceIterateIndexOptions(func(o IndexOptions) {
567 got = o
568 }, func(_ uint32, e error) {
569 err = e
570 }, 123)
571
572 if err != nil && want != nil {
573 t.Fatalf("unexpected error: %v", err)
574 }
575 if want == nil {
576 continue
577 }
578
579 want.CloneURL = sg.getCloneURL(got.Name)
580
581 if d := cmp.Diff(*want, got); d != "" {
582 t.Log("response", r)
583 t.Errorf("mismatch (-want +got):\n%s", d)
584 }
585 }
586
587 // Special case our fingerprint API which doesn't return anything if the
588 // repo hasn't changed.
589 t.Run("unchanged", func(t *testing.T) {
590 response = []byte(``)
591
592 got := false
593 var err error
594 sg.ForceIterateIndexOptions(func(_ IndexOptions) {
595 got = true
596 }, func(_ uint32, e error) {
597 err = e
598 }, 123)
599 if err != nil {
600 t.Fatalf("unexpected error: %v", err)
601 }
602
603 if got {
604 t.Fatalf("expected no options, got %v", got)
605 }
606 })
607 })
608
609 var response []byte
610 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
611 if err := r.ParseForm(); err != nil {
612 http.Error(w, err.Error(), http.StatusBadRequest)
613 return
614 }
615 if got, want := r.URL.String(), "/.internal/search/configuration"; got != want {
616 http.Error(w, fmt.Sprintf("got URL %v want %v", got, want), http.StatusBadRequest)
617 return
618 }
619 if got, want := r.Form, (url.Values{"repoID": []string{"123"}}); !reflect.DeepEqual(got, want) {
620 http.Error(w, fmt.Sprintf("got URL %v want %v", got, want), http.StatusBadRequest)
621 return
622 }
623 _, _ = w.Write(response)
624 }))
625 defer server.Close()
626
627 u, err := url.Parse(server.URL)
628 if err != nil {
629 t.Fatal(err)
630 }
631
632 sg := newSourcegraphClient(u, "", WithBatchSize(0))
633
634 cases := map[string]*IndexOptions{
635 `{"Symbols": true, "LargeFiles": ["foo","bar"]}`: {
636 Symbols: true,
637 LargeFiles: []string{"foo", "bar"},
638 },
639
640 `{"Symbols": false, "LargeFiles": ["foo","bar"]}`: {
641 LargeFiles: []string{"foo", "bar"},
642 },
643
644 `{}`: {},
645
646 `{"Symbols": true}`: {
647 Symbols: true,
648 },
649
650 `{"RepoID": 123}`: {
651 RepoID: 123,
652 },
653
654 `{"Error": "boom"}`: nil,
655 }
656
657 for r, want := range cases {
658 response = []byte(r)
659
660 var got IndexOptions
661 var err error
662 sg.ForceIterateIndexOptions(func(o IndexOptions) {
663 got = o
664 }, func(_ uint32, e error) {
665 err = e
666 }, 123)
667
668 if err != nil && want != nil {
669 t.Fatalf("unexpected error: %v", err)
670 }
671 if want == nil {
672 continue
673 }
674
675 want.CloneURL = sg.getCloneURL(got.Name)
676
677 if d := cmp.Diff(*want, got); d != "" {
678 t.Log("response", r)
679 t.Errorf("mismatch (-want +got):\n%s", d)
680 }
681 }
682
683 // Special case our fingerprint API which doesn't return anything if the
684 // repo hasn't changed.
685 t.Run("unchanged", func(t *testing.T) {
686 response = []byte(``)
687
688 got := false
689 var err error
690 sg.ForceIterateIndexOptions(func(_ IndexOptions) {
691 got = true
692 }, func(_ uint32, e error) {
693 err = e
694 }, 123)
695 if err != nil {
696 t.Fatalf("unexpected error: %v", err)
697 }
698
699 if got {
700 t.Fatalf("expected no options, got %v", got)
701 }
702 })
703}
704
705func TestIndex(t *testing.T) {
706 cases := []struct {
707 name string
708 args indexArgs
709 mockRepositoryMetadata *zoekt.Repository
710 want []string
711 }{{
712 name: "minimal",
713 args: indexArgs{
714 IndexOptions: IndexOptions{
715 Name: "test/repo",
716 CloneURL: "http://api.test/.internal/git/test/repo",
717 Branches: []zoekt.RepositoryBranch{{Name: "HEAD", Version: "deadbeef"}},
718 },
719 },
720 want: []string{
721 "git -c init.defaultBranch=nonExistentBranchBB0FOFCH32 init --bare $TMPDIR/test%2Frepo.git",
722 "git -C $TMPDIR/test%2Frepo.git -c protocol.version=2 -c http.extraHeader=X-Sourcegraph-Actor-UID: internal fetch --depth=1 http://api.test/.internal/git/test/repo deadbeef",
723 "git -C $TMPDIR/test%2Frepo.git update-ref HEAD deadbeef",
724 "git -C $TMPDIR/test%2Frepo.git config zoekt.archived 0",
725 "git -C $TMPDIR/test%2Frepo.git config zoekt.fork 0",
726 "git -C $TMPDIR/test%2Frepo.git config zoekt.name test/repo",
727 "git -C $TMPDIR/test%2Frepo.git config zoekt.priority 0",
728 "git -C $TMPDIR/test%2Frepo.git config zoekt.public 0",
729 "git -C $TMPDIR/test%2Frepo.git config zoekt.repoid 0",
730 "zoekt-git-index -submodules=false -branches HEAD -disable_ctags $TMPDIR/test%2Frepo.git",
731 },
732 }, {
733 name: "minimal-id",
734 args: indexArgs{
735 IndexOptions: IndexOptions{
736 Name: "test/repo",
737 CloneURL: "http://api.test/.internal/git/test/repo",
738 Branches: []zoekt.RepositoryBranch{{Name: "HEAD", Version: "deadbeef"}},
739 RepoID: 123,
740 },
741 },
742 want: []string{
743 "git -c init.defaultBranch=nonExistentBranchBB0FOFCH32 init --bare $TMPDIR/test%2Frepo.git",
744 "git -C $TMPDIR/test%2Frepo.git -c protocol.version=2 -c http.extraHeader=X-Sourcegraph-Actor-UID: internal fetch --depth=1 http://api.test/.internal/git/test/repo deadbeef",
745 "git -C $TMPDIR/test%2Frepo.git update-ref HEAD deadbeef",
746 "git -C $TMPDIR/test%2Frepo.git config zoekt.archived 0",
747 "git -C $TMPDIR/test%2Frepo.git config zoekt.fork 0",
748 "git -C $TMPDIR/test%2Frepo.git config zoekt.name test/repo",
749 "git -C $TMPDIR/test%2Frepo.git config zoekt.priority 0",
750 "git -C $TMPDIR/test%2Frepo.git config zoekt.public 0",
751 "git -C $TMPDIR/test%2Frepo.git config zoekt.repoid 123",
752 "zoekt-git-index -submodules=false -branches HEAD -disable_ctags $TMPDIR/test%2Frepo.git",
753 },
754 }, {
755 name: "all",
756 args: indexArgs{
757 Incremental: true,
758 IndexDir: "/data/index",
759 Parallelism: 4,
760 FileLimit: 123,
761 IndexOptions: IndexOptions{
762 Name: "test/repo",
763 CloneURL: "http://api.test/.internal/git/test/repo",
764 LargeFiles: []string{"foo", "bar"},
765 Symbols: true,
766 Branches: []zoekt.RepositoryBranch{
767 {Name: "HEAD", Version: "deadbeef"},
768 {Name: "dev", Version: "feebdaed"}, // ignored for archive
769 },
770 },
771 },
772 want: []string{
773 "git -c init.defaultBranch=nonExistentBranchBB0FOFCH32 init --bare $TMPDIR/test%2Frepo.git",
774 "git -C $TMPDIR/test%2Frepo.git -c protocol.version=2 -c http.extraHeader=X-Sourcegraph-Actor-UID: internal fetch --depth=1 http://api.test/.internal/git/test/repo deadbeef feebdaed",
775 "git -C $TMPDIR/test%2Frepo.git update-ref HEAD deadbeef",
776 "git -C $TMPDIR/test%2Frepo.git update-ref refs/heads/dev feebdaed",
777 "git -C $TMPDIR/test%2Frepo.git config zoekt.archived 0",
778 "git -C $TMPDIR/test%2Frepo.git config zoekt.fork 0",
779 "git -C $TMPDIR/test%2Frepo.git config zoekt.name test/repo",
780 "git -C $TMPDIR/test%2Frepo.git config zoekt.priority 0",
781 "git -C $TMPDIR/test%2Frepo.git config zoekt.public 0",
782 "git -C $TMPDIR/test%2Frepo.git config zoekt.repoid 0",
783 "zoekt-git-index -submodules=false -incremental -branches HEAD,dev " +
784 "-file_limit 123 -parallelism 4 -index /data/index -require_ctags -large_file foo -large_file bar " +
785 "$TMPDIR/test%2Frepo.git",
786 },
787 }, {
788 name: "delta",
789 args: indexArgs{
790 Incremental: true,
791 IndexDir: "/data/index",
792 Parallelism: 4,
793 FileLimit: 123,
794 UseDelta: true,
795 IndexOptions: IndexOptions{
796 RepoID: 0,
797 Name: "test/repo",
798 CloneURL: "http://api.test/.internal/git/test/repo",
799 LargeFiles: []string{"foo", "bar"},
800 Symbols: true,
801 Branches: []zoekt.RepositoryBranch{
802 {Name: "HEAD", Version: "deadbeef"},
803 {Name: "dev", Version: "feebdaed"},
804 {Name: "release", Version: "12345678"},
805 },
806 },
807 DeltaShardNumberFallbackThreshold: 22,
808 },
809 mockRepositoryMetadata: &zoekt.Repository{
810 ID: 0,
811 Name: "test/repo",
812 Branches: []zoekt.RepositoryBranch{
813 {Name: "HEAD", Version: "oldhead"},
814 {Name: "dev", Version: "olddev"},
815 {Name: "release", Version: "oldrelease"},
816 },
817 },
818 want: []string{
819 "git -c init.defaultBranch=nonExistentBranchBB0FOFCH32 init --bare $TMPDIR/test%2Frepo.git",
820 "git -C $TMPDIR/test%2Frepo.git -c protocol.version=2 -c http.extraHeader=X-Sourcegraph-Actor-UID: internal fetch --depth=1 http://api.test/.internal/git/test/repo deadbeef feebdaed 12345678 oldhead olddev oldrelease",
821 "git -C $TMPDIR/test%2Frepo.git update-ref HEAD deadbeef",
822 "git -C $TMPDIR/test%2Frepo.git update-ref refs/heads/dev feebdaed",
823 "git -C $TMPDIR/test%2Frepo.git update-ref refs/heads/release 12345678",
824 "git -C $TMPDIR/test%2Frepo.git config zoekt.archived 0",
825 "git -C $TMPDIR/test%2Frepo.git config zoekt.fork 0",
826 "git -C $TMPDIR/test%2Frepo.git config zoekt.name test/repo",
827 "git -C $TMPDIR/test%2Frepo.git config zoekt.priority 0",
828 "git -C $TMPDIR/test%2Frepo.git config zoekt.public 0",
829 "git -C $TMPDIR/test%2Frepo.git config zoekt.repoid 0",
830 "zoekt-git-index -submodules=false -incremental -branches HEAD,dev,release " +
831 "-delta -delta_threshold 22 -file_limit 123 -parallelism 4 -index /data/index -require_ctags -large_file foo -large_file bar " +
832 "$TMPDIR/test%2Frepo.git",
833 },
834 }}
835
836 for _, tc := range cases {
837 t.Run(tc.name, func(t *testing.T) {
838
839 var got []string
840 runCmd := func(c *exec.Cmd) error {
841 cmd := strings.Join(c.Args, " ")
842 cmd = strings.ReplaceAll(cmd, filepath.Clean(os.TempDir()), "$TMPDIR")
843 got = append(got, cmd)
844 return nil
845 }
846
847 findRepositoryMetadata := func(args *indexArgs) (repository *zoekt.Repository, metadata *zoekt.IndexMetadata, ok bool, err error) {
848 if tc.mockRepositoryMetadata == nil {
849 return args.BuildOptions().FindRepositoryMetadata()
850 }
851
852 return tc.mockRepositoryMetadata, &zoekt.IndexMetadata{}, true, nil
853 }
854
855 c := gitIndexConfig{
856 runCmd: runCmd,
857 findRepositoryMetadata: findRepositoryMetadata,
858 }
859
860 if err := gitIndex(c, &tc.args, sourcegraphNop{}, logtest.Scoped(t)); err != nil {
861 t.Fatal(err)
862 }
863 if !cmp.Equal(got, tc.want) {
864 t.Errorf("git mismatch (-want +got):\n%s", cmp.Diff(tc.want, got, splitargs))
865 }
866 })
867 }
868}
869
870var splitargs = cmpopts.AcyclicTransformer("splitargs", func(cmd string) []string {
871 return strings.Split(cmd, " ")
872})
873
874type mockGRPCClient struct {
875 mockSearchConfiguration func(context.Context, *proto.SearchConfigurationRequest, ...grpc.CallOption) (*proto.SearchConfigurationResponse, error)
876 mockList func(context.Context, *proto.ListRequest, ...grpc.CallOption) (*proto.ListResponse, error)
877 mockRepositoryRank func(context.Context, *proto.RepositoryRankRequest, ...grpc.CallOption) (*proto.RepositoryRankResponse, error)
878 mockDocumentRanks func(context.Context, *proto.DocumentRanksRequest, ...grpc.CallOption) (*proto.DocumentRanksResponse, error)
879 mockUpdateIndexStatus func(context.Context, *proto.UpdateIndexStatusRequest, ...grpc.CallOption) (*proto.UpdateIndexStatusResponse, error)
880}
881
882func (m *mockGRPCClient) SearchConfiguration(ctx context.Context, in *proto.SearchConfigurationRequest, opts ...grpc.CallOption) (*proto.SearchConfigurationResponse, error) {
883 if m.mockSearchConfiguration != nil {
884 return m.mockSearchConfiguration(ctx, in, opts...)
885 }
886
887 return nil, fmt.Errorf("mock RPC SearchConfiguration not implemented")
888}
889
890func (m *mockGRPCClient) List(ctx context.Context, in *proto.ListRequest, opts ...grpc.CallOption) (*proto.ListResponse, error) {
891 if m.mockList != nil {
892 return m.mockList(ctx, in, opts...)
893 }
894
895 return nil, fmt.Errorf("mock RPC List not implemented")
896}
897
898func (m *mockGRPCClient) RepositoryRank(ctx context.Context, in *proto.RepositoryRankRequest, opts ...grpc.CallOption) (*proto.RepositoryRankResponse, error) {
899 if m.mockRepositoryRank != nil {
900 return m.mockRepositoryRank(ctx, in, opts...)
901 }
902
903 return nil, fmt.Errorf("mock RPC RepositoryRank not implemented")
904}
905
906func (m *mockGRPCClient) DocumentRanks(ctx context.Context, in *proto.DocumentRanksRequest, opts ...grpc.CallOption) (*proto.DocumentRanksResponse, error) {
907 if m.mockDocumentRanks != nil {
908 return m.mockDocumentRanks(ctx, in, opts...)
909 }
910
911 return nil, fmt.Errorf("mock RPC DocumentRanks not implemented")
912}
913
914func (m *mockGRPCClient) UpdateIndexStatus(ctx context.Context, in *proto.UpdateIndexStatusRequest, opts ...grpc.CallOption) (*proto.UpdateIndexStatusResponse, error) {
915 if m.mockUpdateIndexStatus != nil {
916 return m.mockUpdateIndexStatus(ctx, in, opts...)
917 }
918
919 return nil, fmt.Errorf("mock RPC UpdateIndexStatus not implemented")
920}
921
922var _ proto.ZoektConfigurationServiceClient = &mockGRPCClient{}