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

Configure Feed

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

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