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}
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) + 10 (file order)
44 wantScore: 6010,
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) + 10 (file order)
51 wantScore: 7510,
52 },
53 {
54 fileName: "a/config/c/d.go",
55 query: &query.Substring{FileName: true, Pattern: "config"},
56 language: "Go",
57 // 500 (word) + 10 (file order)
58 wantScore: 510,
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) + 10 (file order)
132 wantScore: 6560,
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) + 10 (file order)
140 wantScore: 7010,
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) + 10 (file order)
148 wantScore: 8410,
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) + 10 (file order)
156 wantScore: 8310,
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) + 10 (file order)
164 wantScore: 8210,
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) + 10 (file order)
172 wantScore: 8110,
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) + 10 (file order)
180 wantScore: 8010,
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) + 10 (file order)
189 wantScore: 6810,
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) + 10 (file order)
201 wantScore: 8476.667,
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) + 10 (file order)
215 wantScore: 8710,
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) + 10 (file order)
223 wantScore: 4760,
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) + 10 (file order)
231 wantScore: 8410,
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) + 10 (file order)
239 wantScore: 6910,
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) + 10 (file order)
247 wantScore: 5410,
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) + 10 (file order)
269 wantScore: 6560,
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) + 10 (file order)
277 wantScore: 8410,
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) + 10 (file order)
285 wantScore: 8310,
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) + 10 (file order)
293 wantScore: 8210,
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) + 10 (file order)
301 wantScore: 8110,
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) + 10 (file order)
309 wantScore: 8010,
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) + 10 (file order)
334 wantScore: 8510,
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) + 10 (file order)
342 wantScore: 8410,
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) + 10 (file order)
350 wantScore: 8310,
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) + 10 (file order)
358 wantScore: 8210,
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) + 10 (file order)
366 wantScore: 8110,
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) + 10 (file order)
391 wantScore: 8510,
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) + 10 (file order)
399 wantScore: 8310,
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) + 10 (file order)
416 wantScore: 7860,
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) + 10 (file order)
435 wantScore: 8510,
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) + 10 (file order)
443 wantScore: 8410,
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) + 10 (file order)
451 wantScore: 8210,
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) + 10 (file order)
475 wantScore: 8510,
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) + 10 (file order)
483 wantScore: 8310,
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) + 10 (file order)
491 wantScore: 8210,
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) + 10 (file order)
499 wantScore: 8010,
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) + 10 (file order)
520 wantScore: 8510,
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) + 10 (file order)
531 wantScore: 8410,
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) + 10 (file order)
542 wantScore: 8310,
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) + 10 (file order)
558 wantScore: 8560,
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 := srs.Files[0].Score; 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
649func TestDocumentRanks(t *testing.T) {
650 requireCTags(t)
651 dir := t.TempDir()
652
653 opts := Options{
654 IndexDir: dir,
655 RepositoryDescription: zoekt.Repository{
656 Name: "repo",
657 },
658 DocumentRanksVersion: "ranking",
659 }
660
661 searchQuery := &query.Substring{Content: true, Pattern: "Inner"}
662 exampleJava, err := os.ReadFile("./testdata/example.java")
663 if err != nil {
664 t.Fatal(err)
665 }
666
667 cases := []struct {
668 name string
669 documentRank float64
670 documentRanksWeight float64
671 wantScore float64
672 }{
673 {
674 name: "score with no document ranks",
675 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 10 (file order)
676 wantScore: 7010.00,
677 },
678 {
679 name: "score with document ranks",
680 documentRank: 0.8,
681 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 225 (file rank) + 10 (file order)
682 wantScore: 7235.00,
683 },
684 {
685 name: "score with custom document ranks weight",
686 documentRank: 0.8,
687 documentRanksWeight: 1000.0,
688 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 25.00 (file rank) + 10 (file order)
689 wantScore: 7035.00,
690 },
691 }
692
693 for _, c := range cases {
694 t.Run(c.name, func(t *testing.T) {
695 b, err := NewBuilder(opts)
696 if err != nil {
697 t.Fatalf("NewBuilder: %v", err)
698 }
699
700 err = b.Add(zoekt.Document{Name: "example.java", Content: exampleJava, Ranks: []float64{c.documentRank}})
701 if err != nil {
702 t.Fatal(err)
703 }
704
705 if err := b.Finish(); err != nil {
706 t.Fatalf("Finish: %v", err)
707 }
708
709 ss, err := shards.NewDirectorySearcher(dir)
710 if err != nil {
711 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err)
712 }
713 defer ss.Close()
714
715 srs, err := ss.Search(context.Background(), searchQuery, &zoekt.SearchOptions{
716 UseDocumentRanks: true,
717 DocumentRanksWeight: c.documentRanksWeight,
718 DebugScore: true,
719 })
720 if err != nil {
721 t.Fatal(err)
722 }
723
724 if got, want := len(srs.Files), 1; got != want {
725 t.Fatalf("file matches: want %d, got %d", want, got)
726 }
727
728 if got := srs.Files[0].Score; got != c.wantScore {
729 t.Fatalf("score: want %f, got %f\ndebug: %s\ndebugscore: %s", c.wantScore, got, srs.Files[0].Debug, srs.Files[0].LineMatches[0].DebugScore)
730 }
731 })
732 }
733}
734
735func TestRepoRanks(t *testing.T) {
736 requireCTags(t)
737 dir := t.TempDir()
738
739 opts := Options{
740 IndexDir: dir,
741 RepositoryDescription: zoekt.Repository{
742 Name: "repo",
743 },
744 DocumentRanksVersion: "ranking",
745 }
746
747 searchQuery := &query.Substring{Content: true, Pattern: "Inner"}
748 exampleJava, err := os.ReadFile("./testdata/example.java")
749 if err != nil {
750 t.Fatal(err)
751 }
752
753 cases := []struct {
754 name string
755 repoRank uint16
756 wantScore float64
757 }{
758 {
759 name: "no shard rank",
760 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 10 (file order)
761 wantScore: 7010.00,
762 },
763 {
764 name: "medium shard rank",
765 repoRank: 30000,
766 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 10 (file order) + 9.16 (repo rank)
767 wantScore: 7019.16,
768 },
769 {
770 name: "high shard rank",
771 repoRank: 60000,
772 // 5500 (partial symbol at boundary) + 1000 (Java class) + 500 (word match) + 10 (file order) + 18.31 (repo rank)
773 wantScore: 7028.31,
774 },
775 }
776
777 for _, c := range cases {
778 t.Run(c.name, func(t *testing.T) {
779 opts.RepositoryDescription = zoekt.Repository{
780 Name: "repo",
781 Rank: c.repoRank,
782 }
783
784 b, err := NewBuilder(opts)
785 if err != nil {
786 t.Fatalf("NewBuilder: %v", err)
787 }
788
789 err = b.Add(zoekt.Document{Name: "example.java", Content: exampleJava})
790 if err != nil {
791 t.Fatal(err)
792 }
793
794 if err := b.Finish(); err != nil {
795 t.Fatalf("Finish: %v", err)
796 }
797
798 ss, err := shards.NewDirectorySearcher(dir)
799 if err != nil {
800 t.Fatalf("NewDirectorySearcher(%s): %v", dir, err)
801 }
802 defer ss.Close()
803
804 srs, err := ss.Search(context.Background(), searchQuery, &zoekt.SearchOptions{
805 UseDocumentRanks: true,
806 DebugScore: true,
807 })
808 if err != nil {
809 t.Fatal(err)
810 }
811
812 if got, want := len(srs.Files), 1; got != want {
813 t.Fatalf("file matches: want %d, got %d", want, got)
814 }
815
816 if got := srs.Files[0].Score; math.Abs(got-c.wantScore) >= 0.01 {
817 t.Fatalf("score: want %f, got %f\ndebug: %s\ndebugscore: %s", c.wantScore, got, srs.Files[0].Debug, srs.Files[0].LineMatches[0].DebugScore)
818 }
819 })
820 }
821}