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 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: "foo_test.go", 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}