fork of https://github.com/sourcegraph/zoekt
0

Configure Feed

Select the types of activity you want to include in your feed.

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}