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