fork of https://github.com/sourcegraph/zoekt
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 14 kB View raw
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}