fork of https://github.com/sourcegraph/zoekt
1// Copyright 2021 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 zoekt // import "github.com/sourcegraph/zoekt"
16
17import (
18 "bytes"
19 "encoding/gob"
20 "encoding/json"
21 "reflect"
22 "strings"
23 "testing"
24 "time"
25
26 "github.com/grafana/regexp"
27)
28
29/*
30BenchmarkMinimalRepoListEncodings/slice-8 570 2145665 ns/op 753790 bytes 3981 B/op 0 allocs/op
31BenchmarkMinimalRepoListEncodings/map-8 360 3337522 ns/op 740778 bytes 377777 B/op 13002 allocs/op
32*/
33func BenchmarkMinimalRepoListEncodings(b *testing.B) {
34 size := uint32(13000) // 2021-06-24 rough estimate of number of repos on a replica.
35
36 type Slice struct {
37 ID uint32
38 HasSymbols bool
39 Branches []RepositoryBranch
40 IndexTimeUnix int64
41 }
42
43 branches := []RepositoryBranch{{Name: "HEAD", Version: strings.Repeat("a", 40)}}
44 mapData := make(map[uint32]*MinimalRepoListEntry, size)
45 sliceData := make([]Slice, 0, size)
46 indexTime := time.Now().Unix()
47
48 for id := uint32(1); id <= size; id++ {
49 mapData[id] = &MinimalRepoListEntry{
50 HasSymbols: true,
51 Branches: branches,
52 IndexTimeUnix: indexTime,
53 }
54 sliceData = append(sliceData, Slice{
55 ID: id,
56 HasSymbols: true,
57 Branches: branches,
58 IndexTimeUnix: indexTime,
59 })
60 }
61
62 b.Run("slice", benchmarkEncoding(sliceData))
63
64 b.Run("map", benchmarkEncoding(mapData))
65}
66
67func benchmarkEncoding(data any) func(*testing.B) {
68 return func(b *testing.B) {
69 b.Helper()
70
71 var buf bytes.Buffer
72 enc := gob.NewEncoder(&buf)
73 err := enc.Encode(data)
74 if err != nil {
75 b.Fatal(err)
76 }
77
78 b.ReportAllocs()
79 b.ResetTimer()
80 b.ReportMetric(float64(buf.Len()), "bytes")
81 for i := 0; i < b.N; i++ {
82 _ = enc.Encode(data)
83 buf.Reset()
84 }
85 }
86}
87
88func TestSizeBytesSearchResult(t *testing.T) {
89 sr := SearchResult{
90 Stats: Stats{}, // 129 bytes
91 Progress: Progress{}, // 16 bytes
92 Files: []FileMatch{{ // 24 bytes + 460 bytes
93 Score: 0, // 8 bytes
94 Debug: "", // 16 bytes
95 FileName: "", // 16 bytes
96 Repository: "", // 16 bytes
97 Branches: nil, // 24 bytes
98 LineMatches: nil, // 24 bytes
99 ChunkMatches: []ChunkMatch{{ // 24 bytes + 208 bytes (see TestSizeByteChunkMatches)
100 Content: []byte("foo"),
101 ContentStart: Location{},
102 FileName: false,
103 Ranges: []Range{{}},
104 SymbolInfo: []*Symbol{{}},
105 Score: 0,
106 DebugScore: "",
107 }},
108 RepositoryID: 0, // 4 bytes
109 RepositoryPriority: 0, // 8 bytes
110 Content: nil, // 24 bytes
111 Checksum: nil, // 24 bytes
112 Language: "", // 16 bytes
113 SubRepositoryName: "", // 16 bytes
114 SubRepositoryPath: "", // 16 bytes
115 Version: "", // 16 bytes
116 }},
117 RepoURLs: nil, // 48 bytes
118 LineFragments: nil, // 48 bytes
119 }
120
121 var wantBytes uint64 = 725
122 if sr.SizeBytes() != wantBytes {
123 t.Fatalf("want %d, got %d", wantBytes, sr.SizeBytes())
124 }
125}
126
127func TestSizeBytesChunkMatches(t *testing.T) {
128 cm := ChunkMatch{
129 Content: []byte("foo"), // 24 + 3 bytes
130 ContentStart: Location{}, // 12 bytes
131 FileName: false, // 1 byte
132 Ranges: []Range{{}}, // 24 bytes (slice header) + 24 bytes (content)
133 SymbolInfo: []*Symbol{{}}, // 24 bytes (slice header) + 4 * 16 bytes (string header) + 8 bytes (pointer)
134 Score: 0, // 8 byte
135 DebugScore: "", // 16 bytes (string header)
136 }
137
138 var wantBytes uint64 = 208
139 if cm.sizeBytes() != wantBytes {
140 t.Fatalf("want %d, got %d", wantBytes, cm.sizeBytes())
141 }
142}
143
144func TestMatchSize(t *testing.T) {
145 cases := []struct {
146 v any
147 size int
148 }{{
149 v: FileMatch{},
150 size: 256,
151 }, {
152 v: ChunkMatch{},
153 size: 120,
154 }}
155 for _, c := range cases {
156 got := reflect.TypeOf(c.v).Size()
157 if int(got) != c.size {
158 t.Errorf(`sizeof struct %T has changed from %d to %d.
159These are match structs that occur a lot in memory, so we optimize size.
160When changing, please ensure there isn't unnecessary padding via the
161tool fieldalignment then update this test.`, c.v, c.size, got)
162 }
163 }
164}
165
166func TestSearchOptions_String(t *testing.T) {
167 // To make sure we don't forget to update the string implementation we use
168 // reflection to generate a SearchOptions with every field being non
169 // default. We then check that the field name is present in the output.
170 opts := SearchOptions{}
171 var fieldNames []string
172 rv := reflect.ValueOf(&opts).Elem()
173 for i := range rv.NumField() {
174 f := rv.Field(i)
175 name := rv.Type().Field(i).Name
176 fieldNames = append(fieldNames, name)
177 switch f.Kind() {
178 case reflect.Bool:
179 f.SetBool(true)
180 case reflect.Int:
181 f.SetInt(1)
182 case reflect.Int64:
183 f.SetInt(1)
184 case reflect.Float64:
185 f.SetFloat(1)
186 case reflect.Map:
187 // Only map is SpanContext
188 f.Set(reflect.ValueOf(map[string]string{"key": "value"}))
189 default:
190 t.Fatalf("add support for %s field (%s)", f.Kind(), name)
191 }
192 }
193
194 s := opts.String()
195 for _, name := range fieldNames {
196 found, err := regexp.MatchString("\\b"+regexp.QuoteMeta(name)+"\\b", s)
197 if err != nil {
198 t.Fatal(err)
199 }
200 if !found {
201 t.Errorf("could not find field %q in string output of SearchOptions:\n%s", name, s)
202 }
203 }
204
205 webDefaults := SearchOptions{
206 MaxWallTime: 10 * time.Second,
207 }
208 webDefaults.SetDefaults()
209
210 // Now we hand craft a few corner and common cases
211 cases := []struct {
212 Opts SearchOptions
213 Want string
214 }{{
215 // Empty
216 Opts: SearchOptions{},
217 Want: "zoekt.SearchOptions{ }",
218 }, {
219 // healthz options
220 Opts: SearchOptions{ShardMaxMatchCount: 1, TotalMaxMatchCount: 1, MaxDocDisplayCount: 1},
221 Want: "zoekt.SearchOptions{ ShardMaxMatchCount=1 TotalMaxMatchCount=1 MaxDocDisplayCount=1 }",
222 }, {
223 // zoekt-webserver defaults
224 Opts: webDefaults,
225 Want: "zoekt.SearchOptions{ ShardMaxMatchCount=100000 TotalMaxMatchCount=1000000 MaxWallTime=10s }",
226 }}
227
228 for _, tc := range cases {
229 got := tc.Opts.String()
230 if got != tc.Want {
231 t.Errorf("unexpected String for %#v:\ngot: %s\nwant: %s", tc.Opts, got, tc.Want)
232 }
233 }
234}
235
236func TestRepositoryMergeMutable(t *testing.T) {
237 a := Repository{
238 ID: 0,
239 Name: "name",
240 Branches: []RepositoryBranch{
241 {
242 Name: "branchName",
243 Version: "branchVersion",
244 },
245 },
246 RawConfig: nil,
247 URL: "url",
248 CommitURLTemplate: "commitUrlTemplate",
249 FileURLTemplate: "fileUrlTemplate",
250 LineFragmentTemplate: "lineFragmentTemplate",
251 }
252
253 t.Run("different ID", func(t *testing.T) {
254 b := a
255 b.ID = 1
256 mutated, err := a.MergeMutable(&b)
257 if err == nil {
258 t.Fatalf("want err, got mutated=%t", mutated)
259 }
260 })
261 t.Run("different Name", func(t *testing.T) {
262 b := a
263 b.Name = "otherName"
264 mutated, err := a.MergeMutable(&b)
265 if err == nil {
266 t.Fatalf("want err, got mutated=%t", mutated)
267 }
268 })
269 t.Run("different Branches", func(t *testing.T) {
270 b := a
271 b.Branches = []RepositoryBranch{
272 {
273 Name: "otherBranchName",
274 Version: "branchVersion",
275 },
276 }
277 mutated, err := a.MergeMutable(&b)
278 if err == nil {
279 t.Fatalf("want err, got mutated=%t", mutated)
280 }
281 })
282 t.Run("different RawConfig", func(t *testing.T) {
283 b := a
284 b.RawConfig = map[string]string{"foo": "bar"}
285 mutated, err := a.MergeMutable(&b)
286 if err != nil {
287 t.Fatalf("got err %v", err)
288 }
289 if !mutated {
290 t.Fatalf("want mutated=true, got false")
291 }
292 if !reflect.DeepEqual(a.RawConfig, b.RawConfig) {
293 t.Fatalf("got different RawConfig, %v vs %v", a.RawConfig, b.RawConfig)
294 }
295 })
296 t.Run("different URL", func(t *testing.T) {
297 b := a
298 b.URL = "otherURL"
299 mutated, err := a.MergeMutable(&b)
300 if err != nil {
301 t.Fatalf("got err %v", err)
302 }
303 if !mutated {
304 t.Fatalf("want mutated=true, got false")
305 }
306 if a.URL != b.URL {
307 t.Fatalf("got different URL, %s vs %s", a.URL, b.URL)
308 }
309 })
310 t.Run("different CommitURLTemplate", func(t *testing.T) {
311 b := a
312 b.CommitURLTemplate = "otherCommitUrlTemplate"
313 mutated, err := a.MergeMutable(&b)
314 if err != nil {
315 t.Fatalf("got err %v", err)
316 }
317 if !mutated {
318 t.Fatalf("want mutated=true, got false")
319 }
320 if a.CommitURLTemplate != b.CommitURLTemplate {
321 t.Fatalf("got different CommitURLTemplate, %s vs %s", a.CommitURLTemplate, b.CommitURLTemplate)
322 }
323 })
324 t.Run("different FileURLTemplate", func(t *testing.T) {
325 b := a
326 b.FileURLTemplate = "otherFileUrlTemplate"
327 mutated, err := a.MergeMutable(&b)
328 if err != nil {
329 t.Fatalf("got err %v", err)
330 }
331 if !mutated {
332 t.Fatalf("want mutated=true, got false")
333 }
334 if a.FileURLTemplate != b.FileURLTemplate {
335 t.Fatalf("got different FileURLTemplate, %s vs %s", a.FileURLTemplate, b.FileURLTemplate)
336 }
337 })
338 t.Run("different LineFragmentTemplate", func(t *testing.T) {
339 b := a
340 b.LineFragmentTemplate = "otherLineFragmentTemplate"
341 mutated, err := a.MergeMutable(&b)
342 if err != nil {
343 t.Fatalf("got err %v", err)
344 }
345 if !mutated {
346 t.Fatalf("want mutated=true, got false")
347 }
348 if a.LineFragmentTemplate != b.LineFragmentTemplate {
349 t.Fatalf("got different LineFragmentTemplate, %s vs %s", a.LineFragmentTemplate, b.LineFragmentTemplate)
350 }
351 })
352 t.Run("all same", func(t *testing.T) {
353 b := a
354 mutated, err := a.MergeMutable(&b)
355 if err != nil {
356 t.Fatalf("got err %v", err)
357 }
358 if mutated {
359 t.Fatalf("want mutated=false, got true")
360 }
361 if !reflect.DeepEqual(a, b) {
362 t.Fatalf("got different Repository, %v vs %v", a, b)
363 }
364 })
365}
366
367func TestMonthsSince1970(t *testing.T) {
368 tests := []struct {
369 name string
370 input time.Time
371 expected uint16
372 }{
373 {"Before 1970", time.Date(1950, 12, 31, 0, 0, 0, 0, time.UTC), 0},
374 {"Unix 0", time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), 0},
375 {"Feb 1970", time.Date(1970, 2, 1, 0, 0, 0, 0, time.UTC), 1},
376 {"Year 1989", time.Date(1989, 12, 13, 0, 0, 0, 0, time.UTC), 239},
377 {"Sep 2024", time.Date(2024, 9, 20, 0, 0, 0, 0, time.UTC), 656},
378 {"Oct 2024", time.Date(2024, 10, 20, 0, 0, 0, 0, time.UTC), 657},
379 {"Apr 7431", time.Date(7431, 4, 1, 0, 0, 0, 0, time.UTC), 65535},
380 {"9999", time.Date(9999, 0, 0, 0, 0, 0, 0, time.UTC), 65535},
381 }
382
383 for _, tt := range tests {
384 t.Run(tt.name, func(t *testing.T) {
385 result := monthsSince1970(tt.input)
386 if result != tt.expected {
387 t.Errorf("expected %d, got %d", tt.expected, result)
388 }
389 })
390 }
391}
392
393// TestRepositoryUnmarshalJSONStackOverflowFix tests that the UnmarshalJSON method
394// for Repository doesn't cause a stack overflow due to infinite recursion.
395// This test creates a scenario that would trigger the bug where the type alias
396// was incorrectly defined as a pointer type, causing recursive calls during
397// JSON unmarshaling of nested SubRepoMap structures.
398func TestRepositoryUnmarshalJSONStackOverflowFix(t *testing.T) {
399 // Create a JSON with nested SubRepoMap containing Repository objects
400 // This specifically tests the recursive scenario that causes stack overflow
401 jsonData := `{
402 "id": 12345,
403 "name": "test-repo",
404 "url": "https://example.com/test-repo",
405 "branches": [
406 {
407 "name": "main",
408 "version": "abc123"
409 }
410 ],
411 "rawconfig": {
412 "repoid": "12345"
413 },
414 "subrepomap": {
415 "submodule1": {
416 "id": 11111,
417 "name": "submodule1",
418 "url": "https://example.com/submodule1",
419 "rawconfig": {
420 "repoid": "11111"
421 },
422 "subrepomap": {
423 "nested-submodule": {
424 "id": 33333,
425 "name": "nested-submodule",
426 "rawconfig": {
427 "repoid": "33333"
428 }
429 }
430 }
431 },
432 "submodule2": {
433 "id": 22222,
434 "name": "submodule2",
435 "rawconfig": {
436 "repoid": "22222"
437 }
438 }
439 }
440 }`
441
442 // Attempt to unmarshal the JSON data into a Repository struct
443 // This would previously cause a stack overflow due to incorrect type alias
444 var repo Repository
445 err := json.Unmarshal([]byte(jsonData), &repo)
446
447 // Verify that unmarshaling succeeds without stack overflow
448 if err != nil {
449 t.Fatalf("Failed to unmarshal Repository JSON: %v", err)
450 }
451
452 // Basic verification
453 if repo.ID != 12345 {
454 t.Errorf("Expected ID 12345, got %d", repo.ID)
455 }
456
457 if repo.Name != "test-repo" {
458 t.Errorf("Expected Name 'test-repo', got '%s'", repo.Name)
459 }
460
461 // Verify nested SubRepoMap handling doesn't cause stack overflow
462 if repo.SubRepoMap == nil {
463 t.Fatalf("Expected SubRepoMap to be non-nil")
464 }
465
466 if len(repo.SubRepoMap) != 2 {
467 t.Fatalf("Expected 2 subrepos in SubRepoMap, got %d", len(repo.SubRepoMap))
468 }
469
470 // Verify nested repository unmarshaling worked correctly
471 submodule1, exists := repo.SubRepoMap["submodule1"]
472 if !exists {
473 t.Fatalf("Expected submodule1 to exist in SubRepoMap")
474 }
475
476 if submodule1.ID != 11111 {
477 t.Errorf("Expected submodule1 ID 11111, got %d", submodule1.ID)
478 }
479
480 // Verify deeply nested SubRepoMap works
481 if submodule1.SubRepoMap == nil {
482 t.Fatalf("Expected submodule1 SubRepoMap to be non-nil")
483 }
484
485 nestedSubmodule, exists := submodule1.SubRepoMap["nested-submodule"]
486 if !exists {
487 t.Fatalf("Expected nested-submodule to exist")
488 }
489
490 if nestedSubmodule.ID != 33333 {
491 t.Errorf("Expected nested-submodule ID 33333, got %d", nestedSubmodule.ID)
492 }
493}