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