fork of https://github.com/sourcegraph/zoekt
1// Copyright 2016 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package build
16
17import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "fmt"
22 "log"
23 "os"
24 "path/filepath"
25 "reflect"
26 "runtime"
27 "sort"
28 "strings"
29 "testing"
30 "time"
31
32 "github.com/google/go-cmp/cmp"
33 "github.com/google/go-cmp/cmp/cmpopts"
34 "github.com/grafana/regexp"
35 "github.com/sourcegraph/zoekt"
36 "github.com/sourcegraph/zoekt/query"
37 "github.com/sourcegraph/zoekt/shards"
38)
39
40func TestBasic(t *testing.T) {
41 dir := t.TempDir()
42
43 opts := Options{
44 IndexDir: dir,
45 ShardMax: 1024,
46 RepositoryDescription: zoekt.Repository{
47 Name: "repo",
48 },
49 Parallelism: 2,
50 SizeMax: 1 << 20,
51 }
52
53 b, err := NewBuilder(opts)
54 if err != nil {
55 t.Fatalf("NewBuilder: %v", err)
56 }
57
58 for i := 0; i < 4; i++ {
59 s := fmt.Sprintf("%d", i)
60 if err := b.AddFile("F"+s, []byte(strings.Repeat(s, 1000))); err != nil {
61 t.Fatal(err)
62 }
63 }
64
65 if err := b.Finish(); err != nil {
66 t.Errorf("Finish: %v", err)
67 }
68
69 fs, _ := filepath.Glob(dir + "/*.zoekt")
70 if len(fs) <= 1 {
71 t.Fatalf("want multiple shards, got %v", fs)
72 }
73
74 _, md0, err := zoekt.ReadMetadataPath(fs[0])
75 if err != nil {
76 t.Fatal(err)
77 }
78 for _, f := range fs[1:] {
79 _, md, err := zoekt.ReadMetadataPath(f)
80 if err != nil {
81 t.Fatal(err)
82 }
83 if md.IndexTime != md0.IndexTime {
84 t.Fatalf("wanted identical time stamps but got %v!=%v", md.IndexTime, md0.IndexTime)
85 }
86 if md.ID != md0.ID {
87 t.Fatalf("wanted identical IDs but got %s!=%s", md.ID, md0.ID)
88 }
89 }
90
91 ss, err := shards.NewDirectorySearcher(dir)
92 if err != nil {
93 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err)
94 }
95 defer ss.Close()
96
97 q, err := query.Parse("111")
98 if err != nil {
99 t.Fatalf("Parse(111): %v", err)
100 }
101
102 var sOpts zoekt.SearchOptions
103 ctx := context.Background()
104 result, err := ss.Search(ctx, q, &sOpts)
105 if err != nil {
106 t.Fatalf("Search(%v): %v", q, err)
107 }
108
109 if len(result.Files) != 1 {
110 t.Errorf("got %v, want 1 file.", result.Files)
111 } else if gotFile, wantFile := result.Files[0].FileName, "F1"; gotFile != wantFile {
112 t.Errorf("got file %q, want %q", gotFile, wantFile)
113 } else if gotRepo, wantRepo := result.Files[0].Repository, "repo"; gotRepo != wantRepo {
114 t.Errorf("got repo %q, want %q", gotRepo, wantRepo)
115 }
116
117 t.Run("meta file", func(t *testing.T) {
118 // use retryTest to allow for the directory watcher to notice the meta
119 // file
120 retryTest(t, func(fatalf func(format string, args ...interface{})) {
121 // Add a .meta file for each shard with repo.Name set to
122 // "repo-mutated". We do this inside retry helper since we have noticed
123 // some flakiness on github CI.
124 for _, p := range fs {
125 repos, _, err := zoekt.ReadMetadataPath(p)
126 if err != nil {
127 t.Fatal(err)
128 }
129 repos[0].Name = "repo-mutated"
130 b, err := json.Marshal(repos[0])
131 if err != nil {
132 t.Fatal(err)
133 }
134
135 if err := os.WriteFile(p+".meta", b, 0o600); err != nil {
136 t.Fatal(err)
137 }
138 }
139
140 result, err := ss.Search(ctx, q, &sOpts)
141 if err != nil {
142 fatalf("Search(%v): %v", q, err)
143 }
144
145 if len(result.Files) != 1 {
146 fatalf("got %v, want 1 file.", result.Files)
147 } else if gotFile, wantFile := result.Files[0].FileName, "F1"; gotFile != wantFile {
148 fatalf("got file %q, want %q", gotFile, wantFile)
149 } else if gotRepo, wantRepo := result.Files[0].Repository, "repo-mutated"; gotRepo != wantRepo {
150 fatalf("got repo %q, want %q", gotRepo, wantRepo)
151 }
152 })
153 })
154}
155
156// retryTest will retry f until min(t.Deadline(), time.Minute). It returns
157// once f doesn't call fatalf.
158func retryTest(t *testing.T, f func(fatalf func(format string, args ...interface{}))) {
159 t.Helper()
160
161 sleep := 10 * time.Millisecond
162 deadline := time.Now().Add(time.Minute)
163 if d, ok := t.Deadline(); ok && d.Before(deadline) {
164 // give 1s for us to do a final test run
165 deadline = d.Add(-time.Second)
166 }
167
168 for {
169 done := make(chan bool)
170 go func() {
171 defer close(done)
172
173 f(func(format string, args ...interface{}) {
174 runtime.Goexit()
175 })
176
177 done <- true
178 }()
179
180 success := <-done
181 if success {
182 return
183 }
184
185 // each time we increase sleep by 1.5
186 sleep := sleep*2 - sleep/2
187 if time.Now().Add(sleep).After(deadline) {
188 break
189 }
190 time.Sleep(sleep)
191 }
192
193 // final run for the test, using the real t.Fatalf
194 f(t.Fatalf)
195}
196
197func TestLargeFileOption(t *testing.T) {
198 dir := t.TempDir()
199
200 sizeMax := 1000
201 opts := Options{
202 IndexDir: dir,
203 LargeFiles: []string{"F0", "F1", "F2", "!F1"},
204 RepositoryDescription: zoekt.Repository{
205 Name: "repo",
206 },
207 SizeMax: sizeMax,
208 }
209
210 b, err := NewBuilder(opts)
211 if err != nil {
212 t.Fatalf("NewBuilder: %v", err)
213 }
214
215 for i := 0; i < 4; i++ {
216 s := fmt.Sprintf("%d", i)
217 if err := b.AddFile("F"+s, []byte(strings.Repeat("a", sizeMax+1))); err != nil {
218 t.Fatal(err)
219 }
220 }
221
222 if err := b.Finish(); err != nil {
223 t.Errorf("Finish: %v", err)
224 }
225
226 ss, err := shards.NewDirectorySearcher(dir)
227 if err != nil {
228 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err)
229 }
230
231 q, err := query.Parse("aaa")
232 if err != nil {
233 t.Fatalf("Parse(aaa): %v", err)
234 }
235
236 var sOpts zoekt.SearchOptions
237 ctx := context.Background()
238 result, err := ss.Search(ctx, q, &sOpts)
239 if err != nil {
240 t.Fatalf("Search(%v): %v", q, err)
241 }
242
243 if len(result.Files) != 2 {
244 t.Errorf("got %v files, want 2 files.", len(result.Files))
245 }
246 defer ss.Close()
247}
248
249func TestUpdate(t *testing.T) {
250 dir := t.TempDir()
251
252 opts := Options{
253 IndexDir: dir,
254 ShardMax: 1024,
255 RepositoryDescription: zoekt.Repository{
256 Name: "repo",
257 FileURLTemplate: "url",
258 },
259 Parallelism: 2,
260 SizeMax: 1 << 20,
261 }
262
263 if b, err := NewBuilder(opts); err != nil {
264 t.Fatalf("NewBuilder: %v", err)
265 } else {
266 if err := b.AddFile("F", []byte("hoi")); err != nil {
267 t.Errorf("AddFile: %v", err)
268 }
269 if err := b.Finish(); err != nil {
270 t.Errorf("Finish: %v", err)
271 }
272 }
273 ss, err := shards.NewDirectorySearcher(dir)
274 if err != nil {
275 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err)
276 }
277
278 ctx := context.Background()
279 repos, err := ss.List(ctx, &query.Repo{Regexp: regexp.MustCompile("repo")}, nil)
280 if err != nil {
281 t.Fatalf("List: %v", err)
282 }
283
284 if len(repos.Repos) != 1 {
285 t.Errorf("List(repo): got %v, want 1 repo", repos.Repos)
286 }
287
288 fs, err := filepath.Glob(filepath.Join(dir, "*"))
289 if err != nil {
290 t.Fatalf("glob: %v", err)
291 }
292
293 opts.RepositoryDescription = zoekt.Repository{
294 Name: "repo2",
295 FileURLTemplate: "url2",
296 }
297
298 if b, err := NewBuilder(opts); err != nil {
299 t.Fatalf("NewBuilder: %v", err)
300 } else {
301 if err := b.AddFile("F", []byte("hoi")); err != nil {
302 t.Errorf("AddFile: %v", err)
303 }
304 if err := b.Finish(); err != nil {
305 t.Errorf("Finish: %v", err)
306 }
307 }
308
309 // This is ugly, and potentially flaky, but there is no
310 // observable synchronization for the Sharded searcher, so
311 // this is the best we can do.
312 time.Sleep(100 * time.Millisecond)
313
314 ctx = context.Background()
315 if repos, err = ss.List(ctx, &query.Repo{Regexp: regexp.MustCompile("repo")}, nil); err != nil {
316 t.Fatalf("List: %v", err)
317 } else if len(repos.Repos) != 2 {
318 t.Errorf("List(repo): got %v, want 2 repos", repos.Repos)
319 }
320
321 for _, fn := range fs {
322 log.Printf("removing %s", fn)
323 if err := os.Remove(fn); err != nil {
324 t.Fatalf("Remove(%s): %v", fn, err)
325 }
326 }
327
328 time.Sleep(100 * time.Millisecond)
329
330 ctx = context.Background()
331 if repos, err = ss.List(ctx, &query.Repo{Regexp: regexp.MustCompile("repo")}, nil); err != nil {
332 t.Fatalf("List: %v", err)
333 } else if len(repos.Repos) != 1 {
334 var ss []string
335 for _, r := range repos.Repos {
336 ss = append(ss, r.Repository.Name)
337 }
338 t.Errorf("List(repo): got %v, want 1 repo", ss)
339 }
340}
341
342func TestDeleteOldShards(t *testing.T) {
343 dir := t.TempDir()
344
345 opts := Options{
346 IndexDir: dir,
347 ShardMax: 1024,
348 RepositoryDescription: zoekt.Repository{
349 Name: "repo",
350 FileURLTemplate: "url",
351 },
352 SizeMax: 1 << 20,
353 }
354 opts.SetDefaults()
355
356 b, err := NewBuilder(opts)
357 if err != nil {
358 t.Fatalf("NewBuilder: %v", err)
359 }
360 for i := 0; i < 4; i++ {
361 s := fmt.Sprintf("%d\n", i)
362 if err := b.AddFile("F"+s, []byte(strings.Repeat(s, 1024/2))); err != nil {
363 t.Errorf("AddFile: %v", err)
364 }
365 }
366 if err := b.Finish(); err != nil {
367 t.Errorf("Finish: %v", err)
368 }
369
370 glob := filepath.Join(dir, "*.zoekt")
371 fs, err := filepath.Glob(glob)
372 if err != nil {
373 t.Fatalf("Glob(%s): %v", glob, err)
374 } else if len(fs) != 4 {
375 t.Fatalf("Glob(%s): got %v, want 4 shards", glob, fs)
376 }
377
378 if fi, err := os.Lstat(fs[0]); err != nil {
379 t.Fatalf("Lstat: %v", err)
380 } else if fi.Mode()&0o666 == 0o600 {
381 // This fails spuriously if your umask is very restrictive.
382 t.Errorf("got mode %o, should respect umask.", fi.Mode())
383 }
384
385 // Do again, without sharding.
386 opts.ShardMax = 1 << 20
387 b, err = NewBuilder(opts)
388 if err != nil {
389 t.Fatalf("NewBuilder: %v", err)
390 }
391 for i := 0; i < 4; i++ {
392 s := fmt.Sprintf("%d\n", i)
393 if err := b.AddFile("F"+s, []byte(strings.Repeat(s, 1024/2))); err != nil {
394 t.Fatal(err)
395 }
396 }
397 if err := b.Finish(); err != nil {
398 t.Errorf("Finish: %v", err)
399 }
400
401 fs, err = filepath.Glob(glob)
402 if err != nil {
403 t.Fatalf("Glob(%s): %v", glob, err)
404 } else if len(fs) != 1 {
405 t.Fatalf("Glob(%s): got %v, want 1 shard", glob, fs)
406 }
407
408 // Again, but don't index anything; should leave old shards intact.
409 b, err = NewBuilder(opts)
410 if err != nil {
411 t.Fatalf("NewBuilder: %v", err)
412 }
413 if err := b.Finish(); err != nil {
414 t.Errorf("Finish: %v", err)
415 }
416
417 fs, err = filepath.Glob(glob)
418 if err != nil {
419 t.Fatalf("Glob(%s): %v", glob, err)
420 } else if len(fs) != 1 {
421 t.Fatalf("Glob(%s): got %v, want 1 shard", glob, fs)
422 }
423}
424
425func TestPartialSuccess(t *testing.T) {
426 dir := t.TempDir()
427
428 opts := Options{
429 IndexDir: dir,
430 ShardMax: 1024,
431 SizeMax: 1 << 20,
432 Parallelism: 1,
433 }
434 opts.RepositoryDescription.Name = "repo"
435 opts.SetDefaults()
436
437 b, err := NewBuilder(opts)
438 if err != nil {
439 t.Fatalf("NewBuilder: %v", err)
440 }
441
442 for i := 0; i < 4; i++ {
443 nm := fmt.Sprintf("F%d", i)
444 _ = b.AddFile(nm, []byte(strings.Repeat("01234567\n", 128)))
445 }
446 b.buildError = fmt.Errorf("any error")
447
448 // No error checking.
449 _ = b.Finish()
450
451 // Finish cleans up temporary files.
452 if fs, err := filepath.Glob(dir + "/*"); err != nil {
453 t.Errorf("glob(%s): %v", dir, err)
454 } else if len(fs) != 0 {
455 t.Errorf("got shards %v, want []", fs)
456 }
457}
458
459type filerankCase struct {
460 name string
461 docs []*zoekt.Document
462 want []int
463}
464
465func testFileRankAspect(t *testing.T, c filerankCase) {
466 var want []*zoekt.Document
467 for _, j := range c.want {
468 want = append(want, c.docs[j])
469 }
470
471 got := make([]*zoekt.Document, len(c.docs))
472 copy(got, c.docs)
473 sortDocuments(got)
474
475 print := func(ds []*zoekt.Document) string {
476 r := ""
477 for _, d := range ds {
478 r += fmt.Sprintf("%v, ", d)
479 }
480 return r
481 }
482 if !reflect.DeepEqual(got, want) {
483 t.Errorf("got docs [%v], want [%v]", print(got), print(want))
484 }
485}
486
487func TestFileRank(t *testing.T) {
488 for _, c := range []filerankCase{{
489 name: "filename",
490 docs: []*zoekt.Document{
491 {
492 Name: "longlonglong",
493 Content: []byte("bla"),
494 },
495 {
496 Name: "short",
497 Content: []byte("bla"),
498 },
499 },
500 want: []int{1, 0},
501 }, {
502 name: "test",
503 docs: []*zoekt.Document{
504 {
505 Name: "test",
506 Content: []byte("bla"),
507 },
508 {
509 Name: "longlonglong",
510 Content: []byte("bla"),
511 },
512 },
513 want: []int{1, 0},
514 }, {
515 name: "content",
516 docs: []*zoekt.Document{
517 {
518 Content: []byte("bla"),
519 },
520 {
521 Content: []byte("blablablabla"),
522 },
523 {
524 Content: []byte("blabla"),
525 },
526 },
527 want: []int{0, 2, 1},
528 }, {
529 name: "skipped docs",
530 docs: []*zoekt.Document{
531 {
532 Name: "binary_file",
533 SkipReason: "binary file",
534 },
535 {
536 Name: "some_test.go",
537 Content: []byte("bla"),
538 },
539 {
540 Name: "large_file.go",
541 SkipReason: "too large",
542 },
543 {
544 Name: "file.go",
545 Content: []byte("blabla"),
546 },
547 },
548 want: []int{3, 1, 0, 2},
549 }} {
550 t.Run(c.name, func(t *testing.T) {
551 testFileRankAspect(t, c)
552 })
553 }
554}
555
556func TestEmptyContent(t *testing.T) {
557 dir := t.TempDir()
558
559 opts := Options{
560 IndexDir: dir,
561 RepositoryDescription: zoekt.Repository{
562 Name: "repo",
563 },
564 }
565 opts.SetDefaults()
566
567 b, err := NewBuilder(opts)
568 if err != nil {
569 t.Fatalf("NewBuilder: %v", err)
570 }
571 if err := b.Finish(); err != nil {
572 t.Errorf("Finish: %v", err)
573 }
574
575 fs, _ := filepath.Glob(dir + "/*.zoekt")
576 if len(fs) != 1 {
577 t.Fatalf("want a shard, got %v", fs)
578 }
579
580 ss, err := shards.NewDirectorySearcher(dir)
581 if err != nil {
582 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err)
583 }
584 defer ss.Close()
585
586 ctx := context.Background()
587 result, err := ss.List(ctx, &query.Const{Value: true}, nil)
588 if err != nil {
589 t.Fatalf("List: %v", err)
590 }
591
592 if len(result.Repos) != 1 || result.Repos[0].Repository.Name != "repo" {
593 t.Errorf("got %+v, want 1 repo.", result.Repos)
594 }
595}
596
597func TestDeltaShards(t *testing.T) {
598 // TODO: Need to write a test for compound shards as well.
599 type step struct {
600 name string
601 documents []zoekt.Document
602 optFn func(t *testing.T, o *Options)
603
604 query string
605 expectedDocuments []zoekt.Document
606 }
607
608 var (
609 fooAtMain = zoekt.Document{Name: "foo.go", Branches: []string{"main"}, Content: []byte("common foo-main-v1")}
610 fooAtMainV2 = zoekt.Document{Name: "foo.go", Branches: []string{"main"}, Content: []byte("common foo-main-v2")}
611
612 fooAtMainAndRelease = zoekt.Document{Name: "foo.go", Branches: []string{"main", "release"}, Content: []byte("common foo-main-and-release")}
613
614 barAtMain = zoekt.Document{Name: "bar.go", Branches: []string{"main"}, Content: []byte("common bar-main")}
615 barAtMainV2 = zoekt.Document{Name: "bar.go", Branches: []string{"main"}, Content: []byte("common bar-main-v2")}
616 )
617
618 for _, test := range []struct {
619 name string
620 steps []step
621 }{
622 {
623 name: "tombstone older documents",
624 steps: []step{
625 {
626 name: "setup",
627 documents: []zoekt.Document{barAtMain, fooAtMain},
628 query: "common",
629 expectedDocuments: []zoekt.Document{barAtMain, fooAtMain},
630 },
631 {
632 name: "add new version of foo, tombstone older ones",
633 documents: []zoekt.Document{fooAtMainV2},
634 optFn: func(t *testing.T, o *Options) {
635 o.IsDelta = true
636 o.changedOrRemovedFiles = []string{"foo.go"}
637 },
638 query: "common",
639 expectedDocuments: []zoekt.Document{barAtMain, fooAtMainV2},
640 },
641 {
642 name: "add new version of bar, tombstone older ones",
643 documents: []zoekt.Document{barAtMainV2},
644 optFn: func(t *testing.T, o *Options) {
645 o.IsDelta = true
646 o.changedOrRemovedFiles = []string{"bar.go"}
647 },
648 query: "common",
649 expectedDocuments: []zoekt.Document{barAtMainV2, fooAtMainV2},
650 },
651 },
652 },
653 {
654 name: "tombstone older documents even if the latest shard has no documents",
655 steps: []step{
656 {
657 name: "setup",
658 documents: []zoekt.Document{barAtMain, fooAtMain},
659 query: "common",
660 expectedDocuments: []zoekt.Document{barAtMain, fooAtMain},
661 },
662 {
663 // a build with no documents could represent a deletion
664 name: "tombstone older documents",
665 documents: nil,
666 optFn: func(t *testing.T, o *Options) {
667 o.IsDelta = true
668 o.changedOrRemovedFiles = []string{"foo.go"}
669 },
670 query: "common",
671 expectedDocuments: []zoekt.Document{barAtMain},
672 },
673 },
674 },
675 {
676 name: "tombstones affect document across branches",
677 steps: []step{
678 {
679 name: "setup",
680 documents: []zoekt.Document{barAtMain, fooAtMainAndRelease},
681 query: "common",
682 expectedDocuments: []zoekt.Document{barAtMain, fooAtMainAndRelease},
683 },
684 {
685 name: "tombstone foo",
686 documents: nil,
687 optFn: func(t *testing.T, o *Options) {
688 o.IsDelta = true
689 o.changedOrRemovedFiles = []string{"foo.go"}
690 },
691 query: "common",
692 expectedDocuments: []zoekt.Document{barAtMain},
693 },
694 },
695 },
696 } {
697 t.Run(test.name, func(t *testing.T) {
698 indexDir := t.TempDir()
699
700 branchSet := make(map[string]struct{})
701
702 for _, s := range test.steps {
703 for _, d := range s.documents {
704 for _, b := range d.Branches {
705 branchSet[b] = struct{}{}
706 }
707 }
708 }
709
710 for _, step := range test.steps {
711 repository := zoekt.Repository{ID: 1, Name: "repository"}
712
713 for b := range branchSet {
714 repository.Branches = append(repository.Branches, zoekt.RepositoryBranch{Name: b})
715 }
716
717 sort.Slice(repository.Branches, func(i, j int) bool {
718 a, b := repository.Branches[i], repository.Branches[j]
719
720 return a.Name < b.Name
721 })
722
723 buildOpts := Options{
724 IndexDir: indexDir,
725 RepositoryDescription: repository,
726 }
727 buildOpts.SetDefaults()
728
729 if step.optFn != nil {
730 step.optFn(t, &buildOpts)
731 }
732
733 b, err := NewBuilder(buildOpts)
734 if err != nil {
735 t.Fatalf("step %q: NewBuilder: %s", step.name, err)
736 }
737
738 for _, d := range step.documents {
739 err := b.Add(d)
740 if err != nil {
741 t.Fatalf("step %q: adding document %q to builder: %s", step.name, d.Name, err)
742 }
743 }
744
745 // Call b.Finish() multiple times to ensure that it is idempotent
746 for i := 0; i < 3; i++ {
747
748 err = b.Finish()
749 if err != nil {
750 t.Fatalf("step %q: finishing builder (call #%d): %s", step.name, i, err)
751 }
752 }
753
754 err = b.Finish()
755 if err != nil {
756 t.Fatalf("step %q: finishing builder: %s", step.name, err)
757 }
758
759 state, _ := buildOpts.IndexState()
760 if diff := cmp.Diff(IndexStateEqual, state); diff != "" {
761 t.Errorf("unexpected diff in index state (-want +got):\n%s", diff)
762 }
763
764 ss, err := shards.NewDirectorySearcher(indexDir)
765 if err != nil {
766 t.Fatalf("step %q: NewDirectorySearcher(%s): %s", step.name, indexDir, err)
767 }
768 defer ss.Close()
769
770 searchOpts := &zoekt.SearchOptions{Whole: true}
771 q := &query.Substring{Pattern: step.query}
772
773 result, err := ss.Search(context.Background(), q, searchOpts)
774 if err != nil {
775 t.Fatalf("step %q: Search(%q): %s", step.name, step.query, err)
776 }
777
778 var receivedDocuments []zoekt.Document
779 for _, f := range result.Files {
780 receivedDocuments = append(receivedDocuments, zoekt.Document{
781 Name: f.FileName,
782 Content: f.Content,
783 })
784 }
785
786 cmpOpts := []cmp.Option{
787 cmpopts.IgnoreFields(zoekt.Document{}, "Branches"),
788 cmpopts.SortSlices(func(a, b zoekt.Document) bool {
789 if a.Name < b.Name {
790 return true
791 }
792
793 return bytes.Compare(a.Content, b.Content) < 0
794 }),
795 }
796
797 if diff := cmp.Diff(step.expectedDocuments, receivedDocuments, cmpOpts...); diff != "" {
798 t.Errorf("step %q: diff in received documents (-want +got):%s\n:", step.name, diff)
799 }
800 }
801 })
802 }
803}