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