fork of https://github.com/sourcegraph/zoekt
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}