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