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