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 "context" 19 "math" 20 "os" 21 "testing" 22 23 "github.com/sourcegraph/zoekt" 24 "github.com/sourcegraph/zoekt/ctags" 25 "github.com/sourcegraph/zoekt/query" 26 "github.com/sourcegraph/zoekt/shards" 27) 28 29type scoreCase struct { 30 fileName string 31 content []byte 32 query query.Q 33 language string 34 wantScore float64 35} 36 37func TestFileNameMatch(t *testing.T) { 38 cases := []scoreCase{ 39 { 40 fileName: "a/b/c/config.go", 41 query: &query.Substring{FileName: true, Pattern: "config"}, 42 language: "Go", 43 // 5500 (partial base at boundary) + 500 (word) 44 wantScore: 6000, 45 }, 46 { 47 fileName: "a/b/c/config.go", 48 query: &query.Substring{FileName: true, Pattern: "config.go"}, 49 language: "Go", 50 // 7000 (full base match) + 500 (word) 51 wantScore: 7500, 52 }, 53 { 54 fileName: "a/config/c/d.go", 55 query: &query.Substring{FileName: true, Pattern: "config"}, 56 language: "Go", 57 // 500 (word) 58 wantScore: 500, 59 }, 60 } 61 62 for _, c := range cases { 63 checkScoring(t, c, false, ctags.UniversalCTags) 64 } 65} 66 67func TestBM25(t *testing.T) { 68 exampleJava, err := os.ReadFile("./testdata/example.java") 69 if err != nil { 70 t.Fatal(err) 71 } 72 73 cases := []scoreCase{ 74 { 75 // Matches on both filename and content 76 fileName: "example.java", 77 query: &query.Substring{Pattern: "example"}, 78 content: exampleJava, 79 language: "Java", 80 // bm25-score: 0.57 <- sum-termFrequencyScore: 10.00, length-ratio: 1.00 81 wantScore: 0.57, 82 }, { 83 // Matches only on content 84 fileName: "example.java", 85 query: &query.And{Children: []query.Q{ 86 &query.Substring{Pattern: "inner"}, 87 &query.Substring{Pattern: "static"}, 88 &query.Substring{Pattern: "interface"}, 89 }}, 90 content: exampleJava, 91 language: "Java", 92 // bm25-score: 1.75 <- sum-termFrequencyScore: 56.00, length-ratio: 1.00 93 wantScore: 1.75, 94 }, 95 { 96 // Matches only on filename 97 fileName: "example.java", 98 query: &query.Substring{Pattern: "java"}, 99 content: exampleJava, 100 language: "Java", 101 // bm25-score: 0.51 <- sum-termFrequencyScore: 5.00, length-ratio: 1.00 102 wantScore: 0.51, 103 }, 104 { 105 // Matches only on filename, and content is missing 106 fileName: "a/b/c/config.go", 107 query: &query.Substring{Pattern: "config.go"}, 108 language: "Go", 109 // bm25-score: 0.60 <- sum-termFrequencyScore: 5.00, length-ratio: 0.00 110 wantScore: 0.60, 111 }, 112 } 113 114 for _, c := range cases { 115 checkScoring(t, c, true, ctags.UniversalCTags) 116 } 117} 118 119func TestJava(t *testing.T) { 120 exampleJava, err := os.ReadFile("./testdata/example.java") 121 if err != nil { 122 t.Fatal(err) 123 } 124 125 cases := []scoreCase{ 126 { 127 fileName: "example.java", 128 content: exampleJava, 129 query: &query.Substring{Content: true, Pattern: "nerClass"}, 130 language: "Java", 131 // 5500 (partial symbol at boundary) + 1000 (Java class) + 50 (partial word) 132 wantScore: 6550, 133 }, 134 { 135 fileName: "example.java", 136 content: exampleJava, 137 query: &query.Substring{Content: true, Pattern: "StaticClass"}, 138 language: "Java", 139 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word) 140 wantScore: 7000, 141 }, 142 { 143 fileName: "example.java", 144 content: exampleJava, 145 query: &query.Substring{Content: true, Pattern: "innerEnum"}, 146 language: "Java", 147 // 7000 (symbol) + 900 (Java enum) + 500 (word) 148 wantScore: 8400, 149 }, 150 { 151 fileName: "example.java", 152 content: exampleJava, 153 query: &query.Substring{Content: true, Pattern: "innerInterface"}, 154 language: "Java", 155 // 7000 (symbol) + 800 (Java interface) + 500 (word) 156 wantScore: 8300, 157 }, 158 { 159 fileName: "example.java", 160 content: exampleJava, 161 query: &query.Substring{Content: true, Pattern: "innerMethod"}, 162 language: "Java", 163 // 7000 (symbol) + 700 (Java method) + 500 (word) 164 wantScore: 8200, 165 }, 166 { 167 fileName: "example.java", 168 content: exampleJava, 169 query: &query.Substring{Content: true, Pattern: "field"}, 170 language: "Java", 171 // 7000 (symbol) + 600 (Java field) + 500 (word) 172 wantScore: 8100, 173 }, 174 { 175 fileName: "example.java", 176 content: exampleJava, 177 query: &query.Substring{Content: true, Pattern: "B"}, 178 language: "Java", 179 // 7000 (symbol) + 500 (Java enum constant) + 500 (word) 180 wantScore: 8000, 181 }, 182 // 2 Atoms (1x content and 1x filename) 183 { 184 fileName: "example.java", 185 content: exampleJava, 186 query: &query.Substring{Pattern: "example"}, // matches filename and a Java field 187 language: "Java", 188 // 5500 (edge symbol) + 600 (Java field) + 500 (word) + 200 (atom) 189 wantScore: 6800, 190 }, 191 // 3 Atoms (2x content, 1x filename) 192 { 193 fileName: "example.java", 194 content: exampleJava, 195 query: &query.Or{Children: []query.Q{ 196 &query.Substring{Pattern: "example"}, // matches filename and Java field 197 &query.Substring{Content: true, Pattern: "runInnerInterface"}, // matches a Java method 198 }}, 199 language: "Java", 200 // 7000 (symbol) + 700 (Java method) + 500 (word) + 266.67 (atom) 201 wantScore: 8466, 202 }, 203 // 4 Atoms (4x content) 204 { 205 fileName: "example.java", 206 content: exampleJava, 207 query: &query.Or{Children: []query.Q{ 208 &query.Substring{Content: true, Pattern: "testAnon"}, 209 &query.Substring{Content: true, Pattern: "Override"}, 210 &query.Substring{Content: true, Pattern: "InnerEnum"}, 211 &query.Substring{Content: true, Pattern: "app"}, 212 }}, 213 language: "Java", 214 // 7000 (symbol) + 900 (Java enum) + 500 (word) + 300 (atom) 215 wantScore: 8700, 216 }, 217 { 218 fileName: "example.java", 219 content: exampleJava, 220 query: &query.Substring{Content: true, Pattern: "unInnerInterface("}, 221 language: "Java", 222 // 4000 (overlap Symbol) + 700 (Java method) + 50 (partial word) 223 wantScore: 4750, 224 }, 225 { 226 fileName: "example.java", 227 content: exampleJava, 228 query: &query.Substring{Content: true, Pattern: "InnerEnum"}, 229 language: "Java", 230 // 7000 (Symbol) + 900 (Java enum) + 500 (word) 231 wantScore: 8400, 232 }, 233 { 234 fileName: "example.java", 235 content: exampleJava, 236 query: &query.Substring{Content: true, Pattern: "enum InnerEnum"}, 237 language: "Java", 238 // 5500 (edge Symbol) + 900 (Java enum) + 500 (word) 239 wantScore: 6900, 240 }, 241 { 242 fileName: "example.java", 243 content: exampleJava, 244 query: &query.Substring{Content: true, Pattern: "public enum InnerEnum {"}, 245 language: "Java", 246 // 4000 (overlap Symbol) + 900 (Java enum) + 500 (word) 247 wantScore: 5400, 248 }, 249 } 250 251 for _, c := range cases { 252 checkScoring(t, c, false, ctags.UniversalCTags) 253 } 254} 255 256func TestKotlin(t *testing.T) { 257 exampleKotlin, err := os.ReadFile("./testdata/example.kt") 258 if err != nil { 259 t.Fatal(err) 260 } 261 262 cases := []scoreCase{ 263 { 264 fileName: "example.kt", 265 content: exampleKotlin, 266 query: &query.Substring{Content: true, Pattern: "oxyPreloader"}, 267 language: "Kotlin", 268 // 5500 (partial symbol at boundary) + 1000 (Kotlin class) + 50 (partial word) 269 wantScore: 6550, 270 }, 271 { 272 fileName: "example.kt", 273 content: exampleKotlin, 274 query: &query.Substring{Content: true, Pattern: "ViewMetadata"}, 275 language: "Kotlin", 276 // 7000 (symbol) + 900 (Kotlin interface) + 500 (word) 277 wantScore: 8400, 278 }, 279 { 280 fileName: "example.kt", 281 content: exampleKotlin, 282 query: &query.Substring{Content: true, Pattern: "onScrolled"}, 283 language: "Kotlin", 284 // 7000 (symbol) + 800 (Kotlin method) + 500 (word) 285 wantScore: 8300, 286 }, 287 { 288 fileName: "example.kt", 289 content: exampleKotlin, 290 query: &query.Substring{Content: true, Pattern: "PreloadErrorHandler"}, 291 language: "Kotlin", 292 // 7000 (symbol) + 700 (Kotlin typealias) + 500 (word) 293 wantScore: 8200, 294 }, 295 { 296 fileName: "example.kt", 297 content: exampleKotlin, 298 query: &query.Substring{Content: true, Pattern: "FLING_THRESHOLD_PX"}, 299 language: "Kotlin", 300 // 7000 (symbol) + 600 (Kotlin constant) + 500 (word) 301 wantScore: 8100, 302 }, 303 { 304 fileName: "example.kt", 305 content: exampleKotlin, 306 query: &query.Substring{Content: true, Pattern: "scrollState"}, 307 language: "Kotlin", 308 // 7000 (symbol) + 500 (Kotlin variable) + 500 (word) 309 wantScore: 8000, 310 }, 311 } 312 313 parserType := ctags.UniversalCTags 314 for _, c := range cases { 315 t.Run(c.language, func(t *testing.T) { 316 checkScoring(t, c, false, parserType) 317 }) 318 } 319} 320 321func TestCpp(t *testing.T) { 322 exampleCpp, err := os.ReadFile("./testdata/example.cc") 323 if err != nil { 324 t.Fatal(err) 325 } 326 327 cases := []scoreCase{ 328 { 329 fileName: "example.cc", 330 content: exampleCpp, 331 query: &query.Substring{Content: true, Pattern: "FooClass"}, 332 language: "C++", 333 // 7000 (Symbol) + 1000 (C++ class) + 500 (full word) 334 wantScore: 8500, 335 }, 336 { 337 fileName: "example.cc", 338 content: exampleCpp, 339 query: &query.Substring{Content: true, Pattern: "NestedEnum"}, 340 language: "C++", 341 // 7000 (Symbol) + 900 (C++ enum) + 500 (full word) 342 wantScore: 8400, 343 }, 344 { 345 fileName: "example.cc", 346 content: exampleCpp, 347 query: &query.Substring{Content: true, Pattern: "main"}, 348 language: "C++", 349 // 7000 (Symbol) + 800 (C++ function) + 500 (full word) 350 wantScore: 8300, 351 }, 352 { 353 fileName: "example.cc", 354 content: exampleCpp, 355 query: &query.Substring{Content: true, Pattern: "FooStruct"}, 356 language: "C++", 357 // 7000 (Symbol) + 700 (C++ struct) + 500 (full word) 358 wantScore: 8200, 359 }, 360 { 361 fileName: "example.cc", 362 content: exampleCpp, 363 query: &query.Substring{Content: true, Pattern: "TheUnion"}, 364 language: "C++", 365 // 7000 (Symbol) + 600 (C++ union) + 500 (full word) 366 wantScore: 8100, 367 }, 368 } 369 370 parserType := ctags.UniversalCTags 371 for _, c := range cases { 372 t.Run(c.language, func(t *testing.T) { 373 checkScoring(t, c, false, parserType) 374 }) 375 } 376} 377 378func TestPython(t *testing.T) { 379 examplePython, err := os.ReadFile("./testdata/example.py") 380 if err != nil { 381 t.Fatal(err) 382 } 383 384 cases := []scoreCase{ 385 { 386 fileName: "example.py", 387 content: examplePython, 388 query: &query.Substring{Content: true, Pattern: "C1"}, 389 language: "Python", 390 // 7000 (symbol) + 1000 (Python class) + 500 (word) 391 wantScore: 8500, 392 }, 393 { 394 fileName: "example.py", 395 content: examplePython, 396 query: &query.Substring{Content: true, Pattern: "g"}, 397 language: "Python", 398 // 7000 (symbol) + 800 (Python function) + 500 (word) 399 wantScore: 8300, 400 }, 401 } 402 403 for _, parserType := range []ctags.CTagsParserType{ctags.UniversalCTags, ctags.ScipCTags} { 404 for _, c := range cases { 405 checkScoring(t, c, false, parserType) 406 } 407 } 408 409 // Only test SCIP, as universal-ctags doesn't correctly recognize this as a method 410 scipOnlyCase := scoreCase{ 411 fileName: "example.py", 412 content: examplePython, 413 query: &query.Substring{Content: true, Pattern: "__init__"}, 414 language: "Python", 415 // 7000 (symbol) + 800 (Python method) + 50 (partial word) 416 wantScore: 7850, 417 } 418 419 checkScoring(t, scipOnlyCase, false, ctags.ScipCTags) 420} 421 422func TestRuby(t *testing.T) { 423 exampleRuby, err := os.ReadFile("./testdata/example.rb") 424 if err != nil { 425 t.Fatal(err) 426 } 427 428 cases := []scoreCase{ 429 { 430 fileName: "example.rb", 431 content: exampleRuby, 432 query: &query.Substring{Content: true, Pattern: "Parental"}, 433 language: "Ruby", 434 // 7000 (symbol) + 1000 (Ruby class) + 500 (word) 435 wantScore: 8500, 436 }, 437 { 438 fileName: "example.rb", 439 content: exampleRuby, 440 query: &query.Substring{Content: true, Pattern: "parental_func"}, 441 language: "Ruby", 442 // 7000 (symbol) + 900 (Ruby method) + 500 (word) 443 wantScore: 8400, 444 }, 445 { 446 fileName: "example.rb", 447 content: exampleRuby, 448 query: &query.Substring{Content: true, Pattern: "MyModule"}, 449 language: "Ruby", 450 // 7000 (symbol) + 500 (Ruby module) + 500 (word) 451 wantScore: 8200, 452 }, 453 } 454 455 for _, parserType := range []ctags.CTagsParserType{ctags.UniversalCTags, ctags.ScipCTags} { 456 for _, c := range cases { 457 checkScoring(t, c, false, parserType) 458 } 459 } 460} 461 462func TestScala(t *testing.T) { 463 exampleScala, err := os.ReadFile("./testdata/example.scala") 464 if err != nil { 465 t.Fatal(err) 466 } 467 468 cases := []scoreCase{ 469 { 470 fileName: "example.scala", 471 content: exampleScala, 472 query: &query.Substring{Content: true, Pattern: "SymbolIndexBucket"}, 473 language: "Scala", 474 // 7000 (symbol) + 1000 (Scala class) + 500 (word) 475 wantScore: 8500, 476 }, 477 { 478 fileName: "example.scala", 479 content: exampleScala, 480 query: &query.Substring{Content: true, Pattern: "stdLibPatches"}, 481 language: "Scala", 482 // 7000 (symbol) + 800 (Scala object) + 500 (word) 483 wantScore: 8300, 484 }, 485 { 486 fileName: "example.scala", 487 content: exampleScala, 488 query: &query.Substring{Content: true, Pattern: "close"}, 489 language: "Scala", 490 // 7000 (symbol) + 700 (Scala method) + 500 (word) 491 wantScore: 8200, 492 }, 493 { 494 fileName: "example.scala", 495 content: exampleScala, 496 query: &query.Substring{Content: true, Pattern: "javaSymbol"}, 497 language: "Scala", 498 // 7000 (symbol) + 500 (Scala method) + 500 (word) 499 wantScore: 8000, 500 }, 501 } 502 503 parserType := ctags.UniversalCTags 504 for _, c := range cases { 505 checkScoring(t, c, false, parserType) 506 } 507} 508 509func TestGo(t *testing.T) { 510 cases := []scoreCase{ 511 { 512 fileName: "src/net/http/client.go", 513 content: []byte(` 514package http 515type aInterface interface {} 516`), 517 query: &query.Substring{Content: true, Pattern: "aInterface"}, 518 language: "Go", 519 // 7000 (full base match) + 1000 (Go interface) + 500 (word) 520 wantScore: 8500, 521 }, 522 { 523 fileName: "src/net/http/client.go", 524 content: []byte(` 525package http 526type aStruct struct {} 527`), 528 query: &query.Substring{Content: true, Pattern: "aStruct"}, 529 language: "Go", 530 // 7000 (full base match) + 900 (Go struct) + 500 (word) 531 wantScore: 8400, 532 }, 533 { 534 fileName: "src/net/http/client.go", 535 content: []byte(` 536package http 537func aFunc() bool {} 538`), 539 query: &query.Substring{Content: true, Pattern: "aFunc"}, 540 language: "Go", 541 // 7000 (full base match) + 800 (Go function) + 500 (word) 542 wantScore: 8300, 543 }, 544 { 545 fileName: "src/net/http/client.go", 546 content: []byte(` 547package http 548func Get() { 549 panic("") 550} 551`), 552 query: &query.And{Children: []query.Q{ 553 &query.Symbol{Expr: &query.Substring{Pattern: "http", Content: true}}, 554 &query.Symbol{Expr: &query.Substring{Pattern: "Get", Content: true}}, 555 }}, 556 language: "Go", 557 // 7000 (full base match) + 800 (Go func) + 50 (Exported Go) + 500 (word) + 200 (atom) 558 wantScore: 8550, 559 }, 560 } 561 562 for _, parserType := range []ctags.CTagsParserType{ctags.UniversalCTags, ctags.ScipCTags} { 563 for _, c := range cases { 564 checkScoring(t, c, false, parserType) 565 } 566 } 567} 568 569func skipIfCTagsUnavailable(t *testing.T, parserType ctags.CTagsParserType) { 570 // Never skip universal-ctags tests in CI 571 if os.Getenv("CI") != "" && parserType == ctags.UniversalCTags { 572 return 573 } 574 575 switch parserType { 576 case ctags.UniversalCTags: 577 requireCTags(t) 578 case ctags.ScipCTags: 579 if checkScipCTags() == "" { 580 t.Skip("scip-ctags not available") 581 } 582 default: 583 t.Fatalf("unexpected parser type") 584 } 585} 586 587func checkScoring(t *testing.T, c scoreCase, useBM25 bool, parserType ctags.CTagsParserType) { 588 skipIfCTagsUnavailable(t, parserType) 589 590 name := c.language 591 if parserType == ctags.ScipCTags { 592 name += "-scip" 593 } 594 595 t.Run(name, func(t *testing.T) { 596 dir := t.TempDir() 597 598 opts := Options{ 599 IndexDir: dir, 600 RepositoryDescription: zoekt.Repository{ 601 Name: "repo", 602 }, 603 LanguageMap: ctags.LanguageMap{ 604 normalizeLanguage(c.language): parserType, 605 }, 606 } 607 608 epsilon := 0.01 609 610 b, err := NewBuilder(opts) 611 if err != nil { 612 t.Fatalf("NewBuilder: %v", err) 613 } 614 if err := b.AddFile(c.fileName, c.content); err != nil { 615 t.Fatal(err) 616 } 617 if err := b.Finish(); err != nil { 618 t.Fatalf("Finish: %v", err) 619 } 620 621 ss, err := shards.NewDirectorySearcher(dir) 622 if err != nil { 623 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err) 624 } 625 defer ss.Close() 626 627 srs, err := ss.Search(context.Background(), c.query, &zoekt.SearchOptions{ 628 UseBM25Scoring: useBM25, 629 ChunkMatches: true, 630 DebugScore: true}) 631 if err != nil { 632 t.Fatal(err) 633 } 634 635 if got, want := len(srs.Files), 1; got != want { 636 t.Fatalf("file matches: want %d, got %d", want, got) 637 } 638 639 if got := withoutTiebreaker(srs.Files[0].Score, useBM25); math.Abs(got-c.wantScore) > epsilon { 640 t.Fatalf("score: want %f, got %f\ndebug: %s\ndebugscore: %s", c.wantScore, got, srs.Files[0].Debug, srs.Files[0].ChunkMatches[0].DebugScore) 641 } 642 643 if got := srs.Files[0].Language; got != c.language { 644 t.Fatalf("want %s, got %s", c.language, got) 645 } 646 }) 647} 648 649// helper to remove the tiebreaker from the score for easier comparison 650func withoutTiebreaker(fullScore float64, useBM25 bool) float64 { 651 if useBM25 { 652 return fullScore 653 } 654 return math.Trunc(fullScore / zoekt.ScoreOffset) 655} 656 657func TestDocumentRanks(t *testing.T) { 658 requireCTags(t) 659 dir := t.TempDir() 660 661 opts := Options{ 662 IndexDir: dir, 663 RepositoryDescription: zoekt.Repository{ 664 Name: "repo", 665 }, 666 DocumentRanksVersion: "ranking", 667 } 668 669 searchQuery := &query.Substring{Content: true, Pattern: "Inner"} 670 exampleJava, err := os.ReadFile("./testdata/example.java") 671 if err != nil { 672 t.Fatal(err) 673 } 674 675 cases := []struct { 676 name string 677 documentRank float64 678 documentRanksWeight float64 679 wantScore float64 680 }{ 681 { 682 name: "score with no document ranks", 683 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) 684 wantScore: 7000.00, 685 }, 686 { 687 name: "score with document ranks", 688 documentRank: 0.8, 689 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 225 (file rank) 690 wantScore: 7225.00, 691 }, 692 { 693 name: "score with custom document ranks weight", 694 documentRank: 0.8, 695 documentRanksWeight: 1000.0, 696 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 25.00 (file rank) 697 wantScore: 7025.00, 698 }, 699 } 700 701 for _, c := range cases { 702 t.Run(c.name, func(t *testing.T) { 703 b, err := NewBuilder(opts) 704 if err != nil { 705 t.Fatalf("NewBuilder: %v", err) 706 } 707 708 err = b.Add(zoekt.Document{Name: "example.java", Content: exampleJava, Ranks: []float64{c.documentRank}}) 709 if err != nil { 710 t.Fatal(err) 711 } 712 713 if err := b.Finish(); err != nil { 714 t.Fatalf("Finish: %v", err) 715 } 716 717 ss, err := shards.NewDirectorySearcher(dir) 718 if err != nil { 719 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err) 720 } 721 defer ss.Close() 722 723 srs, err := ss.Search(context.Background(), searchQuery, &zoekt.SearchOptions{ 724 UseDocumentRanks: true, 725 DocumentRanksWeight: c.documentRanksWeight, 726 DebugScore: true, 727 }) 728 if err != nil { 729 t.Fatal(err) 730 } 731 732 if got, want := len(srs.Files), 1; got != want { 733 t.Fatalf("file matches: want %d, got %d", want, got) 734 } 735 736 if got := withoutTiebreaker(srs.Files[0].Score, false); got != c.wantScore { 737 t.Fatalf("score: want %f, got %f\ndebug: %s\ndebugscore: %s", c.wantScore, got, srs.Files[0].Debug, srs.Files[0].LineMatches[0].DebugScore) 738 } 739 }) 740 } 741} 742 743func TestRepoRanks(t *testing.T) { 744 requireCTags(t) 745 dir := t.TempDir() 746 747 opts := Options{ 748 IndexDir: dir, 749 RepositoryDescription: zoekt.Repository{ 750 Name: "repo", 751 }, 752 DocumentRanksVersion: "ranking", 753 } 754 755 searchQuery := &query.Substring{Content: true, Pattern: "Inner"} 756 exampleJava, err := os.ReadFile("./testdata/example.java") 757 if err != nil { 758 t.Fatal(err) 759 } 760 761 cases := []struct { 762 name string 763 repoRank uint16 764 wantScore float64 765 }{ 766 { 767 name: "no shard rank", 768 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 10 (file order) 769 wantScore: 7000_00000_10.00, 770 }, 771 { 772 name: "medium shard rank", 773 repoRank: 30000, 774 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 30000 (repo rank) + 10 (file order) 775 wantScore: 7000_30000_10.00, 776 }, 777 { 778 name: "high shard rank", 779 repoRank: 60000, 780 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 60000 (repo rank) + 10 (file order) 781 wantScore: 7000_60000_10.00, 782 }, 783 } 784 785 for _, c := range cases { 786 t.Run(c.name, func(t *testing.T) { 787 opts.RepositoryDescription = zoekt.Repository{ 788 Name: "repo", 789 Rank: c.repoRank, 790 } 791 792 b, err := NewBuilder(opts) 793 if err != nil { 794 t.Fatalf("NewBuilder: %v", err) 795 } 796 797 err = b.Add(zoekt.Document{Name: "example.java", Content: exampleJava}) 798 if err != nil { 799 t.Fatal(err) 800 } 801 802 if err := b.Finish(); err != nil { 803 t.Fatalf("Finish: %v", err) 804 } 805 806 ss, err := shards.NewDirectorySearcher(dir) 807 if err != nil { 808 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err) 809 } 810 defer ss.Close() 811 812 srs, err := ss.Search(context.Background(), searchQuery, &zoekt.SearchOptions{ 813 UseDocumentRanks: true, 814 DebugScore: true, 815 }) 816 if err != nil { 817 t.Fatal(err) 818 } 819 820 if got, want := len(srs.Files), 1; got != want { 821 t.Fatalf("file matches: want %d, got %d", want, got) 822 } 823 824 if got := srs.Files[0].Score; math.Abs(got-c.wantScore) >= 0.01 { 825 t.Fatalf("score: want %f, got %f\ndebug: %s\ndebugscore: %s", c.wantScore, got, srs.Files[0].Debug, srs.Files[0].LineMatches[0].DebugScore) 826 } 827 }) 828 } 829}