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 zoekt // import "github.com/sourcegraph/zoekt"
16
17import (
18 "bytes"
19 _ "embed"
20 "encoding/gob"
21 "fmt"
22 "math/rand"
23 "reflect"
24 "testing"
25 "testing/quick"
26 "time"
27
28 fuzz "github.com/AdaLogics/go-fuzz-headers"
29 "github.com/google/go-cmp/cmp"
30 "github.com/google/go-cmp/cmp/cmpopts"
31 "google.golang.org/protobuf/proto"
32
33 webserverv1 "github.com/sourcegraph/zoekt/grpc/protos/zoekt/webserver/v1"
34)
35
36func TestProtoRoundtrip(t *testing.T) {
37 t.Run("FileMatch", func(t *testing.T) {
38 f := func(f1 FileMatch) bool {
39 p1 := f1.ToProto()
40 f2 := FileMatchFromProto(p1)
41 return reflect.DeepEqual(f1, f2)
42 }
43 if err := quick.Check(f, nil); err != nil {
44 t.Fatal(err)
45 }
46 })
47
48 t.Run("ChunkMatch", func(t *testing.T) {
49 f := func(f1 ChunkMatch) bool {
50 p1 := f1.ToProto()
51 f2 := ChunkMatchFromProto(p1)
52 return reflect.DeepEqual(f1, f2)
53 }
54 if err := quick.Check(f, nil); err != nil {
55 t.Fatal(err)
56 }
57 })
58
59 t.Run("Range", func(t *testing.T) {
60 f := func(f1 Range) bool {
61 p1 := f1.ToProto()
62 f2 := RangeFromProto(p1)
63 return reflect.DeepEqual(f1, f2)
64 }
65 if err := quick.Check(f, nil); err != nil {
66 t.Fatal(err)
67 }
68 })
69
70 t.Run("Location", func(t *testing.T) {
71 f := func(f1 Range) bool {
72 p1 := f1.ToProto()
73 f2 := RangeFromProto(p1)
74 return reflect.DeepEqual(f1, f2)
75 }
76 if err := quick.Check(f, nil); err != nil {
77 t.Fatal(err)
78 }
79 })
80
81 t.Run("LineMatch", func(t *testing.T) {
82 f := func(f1 LineMatch) bool {
83 p1 := f1.ToProto()
84 f2 := LineMatchFromProto(p1)
85 return reflect.DeepEqual(f1, f2)
86 }
87 if err := quick.Check(f, nil); err != nil {
88 t.Fatal(err)
89 }
90 })
91
92 t.Run("Symbol", func(t *testing.T) {
93 f := func(f1 *Symbol) bool {
94 p1 := f1.ToProto()
95 f2 := SymbolFromProto(p1)
96 return reflect.DeepEqual(f1, f2)
97 }
98 if err := quick.Check(f, nil); err != nil {
99 t.Fatal(err)
100 }
101 })
102
103 t.Run("FlushReson", func(t *testing.T) {
104 f := func(f1 FlushReason) bool {
105 p1 := f1.ToProto()
106 f2 := FlushReasonFromProto(p1)
107 return reflect.DeepEqual(f1.String(), f2.String())
108 }
109 if err := quick.Check(f, nil); err != nil {
110 t.Fatal(err)
111 }
112 })
113
114 t.Run("Stats", func(t *testing.T) {
115 f := func(f1 Stats) bool {
116 p1 := f1.ToProto()
117 f2 := StatsFromProto(p1)
118 return reflect.DeepEqual(f1, f2)
119 }
120 if err := quick.Check(f, nil); err != nil {
121 t.Fatal(err)
122 }
123 })
124
125 t.Run("Progress", func(t *testing.T) {
126 f := func(f1 Progress) bool {
127 p1 := f1.ToProto()
128 f2 := ProgressFromProto(p1)
129 return reflect.DeepEqual(f1, f2)
130 }
131 if err := quick.Check(f, nil); err != nil {
132 t.Fatal(err)
133 }
134 })
135
136 t.Run("SearchResult", func(t *testing.T) {
137 t.Run("unary", func(t *testing.T) {
138 f := func(f1 *SearchResult) bool {
139 var repoURLs map[string]string
140 var lineFragments map[string]string
141
142 if f1 != nil {
143 repoURLs = f1.RepoURLs
144 lineFragments = f1.LineFragments
145 }
146
147 p1 := f1.ToProto()
148 f2 := SearchResultFromProto(p1, repoURLs, lineFragments)
149
150 return reflect.DeepEqual(f1, f2)
151 }
152 if err := quick.Check(f, nil); err != nil {
153 t.Fatal(err)
154 }
155 })
156
157 t.Run("stream", func(t *testing.T) {
158 f := func(f1 *SearchResult) bool {
159 var repoURLs map[string]string
160 var lineFragments map[string]string
161
162 if f1 != nil {
163 repoURLs = f1.RepoURLs
164 lineFragments = f1.LineFragments
165 }
166
167 p1 := f1.ToStreamProto()
168 f2 := SearchResultFromStreamProto(p1, repoURLs, lineFragments)
169
170 return reflect.DeepEqual(f1, f2)
171 }
172 if err := quick.Check(f, nil); err != nil {
173 t.Fatal(err)
174 }
175 })
176 })
177
178 t.Run("Repository", func(t *testing.T) {
179 f := func(f1 *Repository) bool {
180 p1 := f1.ToProto()
181 f2 := RepositoryFromProto(p1)
182 if diff := cmp.Diff(f1, &f2, cmpopts.IgnoreUnexported(Repository{})); diff != "" {
183 fmt.Printf("got diff: %s", diff)
184 return false
185 }
186 return true
187 }
188 if err := quick.Check(f, nil); err != nil {
189 t.Fatal(err)
190 }
191 })
192
193 t.Run("IndexMetadata", func(t *testing.T) {
194 f := func(f1 *IndexMetadata) bool {
195 p1 := f1.ToProto()
196 f2 := IndexMetadataFromProto(p1)
197 if diff := cmp.Diff(f1, &f2); diff != "" {
198 fmt.Printf("got diff: %s", diff)
199 return false
200 }
201 return true
202 }
203 if err := quick.Check(f, nil); err != nil {
204 t.Fatal(err)
205 }
206 })
207
208 t.Run("RepoStats", func(t *testing.T) {
209 f := func(f1 RepoStats) bool {
210 p1 := f1.ToProto()
211 f2 := RepoStatsFromProto(p1)
212 if diff := cmp.Diff(f1, f2); diff != "" {
213 fmt.Printf("got diff: %s", diff)
214 return false
215 }
216 return true
217 }
218 if err := quick.Check(f, nil); err != nil {
219 t.Fatal(err)
220 }
221 })
222
223 t.Run("RepoListEntry", func(t *testing.T) {
224 r1 := &RepoListEntry{
225 Repository: Repository{
226 ID: 1,
227 Name: "test",
228 URL: "testurl",
229 Source: "testsource",
230 Branches: []RepositoryBranch{{
231 Name: "branch",
232 Version: "version",
233 }},
234 SubRepoMap: map[string]*Repository{
235 "test": {
236 ID: 2,
237 Name: "subrepo",
238 Branches: []RepositoryBranch{},
239 SubRepoMap: map[string]*Repository{},
240 FileTombstones: map[string]struct{}{},
241 },
242 },
243 CommitURLTemplate: "committemplate",
244 FileURLTemplate: "fileurltemplate",
245 LineFragmentTemplate: "linefragmenttemplate",
246 priority: 10,
247 RawConfig: map[string]string{
248 "a": "b",
249 },
250 Rank: 32,
251 IndexOptions: "indexoptions",
252 HasSymbols: true,
253 Tombstone: false,
254 LatestCommitDate: time.Now(),
255 FileTombstones: map[string]struct{}{
256 "test1": {},
257 },
258 },
259 IndexMetadata: IndexMetadata{
260 IndexFormatVersion: 32,
261 IndexFeatureVersion: 42,
262 IndexMinReaderVersion: 52,
263 IndexTime: time.Now(),
264 PlainASCII: true,
265 LanguageMap: map[string]uint16{
266 "go": 1,
267 },
268 ZoektVersion: "32",
269 ID: "52",
270 },
271 Stats: RepoStats{
272 Repos: 3,
273 Shards: 4,
274 Documents: 5,
275 IndexBytes: 6,
276 ContentBytes: 7,
277 NewLinesCount: 8,
278 DefaultBranchNewLinesCount: 9,
279 OtherBranchesNewLinesCount: 10,
280 },
281 }
282
283 p1 := r1.ToProto()
284 r2 := RepoListEntryFromProto(p1)
285 if diff := cmp.Diff(r1, r2, cmpopts.IgnoreUnexported(Repository{})); diff != "" {
286 t.Fatalf("got diff: %s", diff)
287 }
288 })
289
290 t.Run("RepositoryBranch", func(t *testing.T) {
291 f := func(f1 RepositoryBranch) bool {
292 p1 := f1.ToProto()
293 f2 := RepositoryBranchFromProto(p1)
294 if diff := cmp.Diff(f1, f2); diff != "" {
295 fmt.Printf("got diff: %s", diff)
296 return false
297 }
298 return true
299 }
300 if err := quick.Check(f, nil); err != nil {
301 t.Fatal(err)
302 }
303 })
304
305 t.Run("MinimalRepoListEntry", func(t *testing.T) {
306 f := func(f1 MinimalRepoListEntry) bool {
307 p1 := f1.ToProto()
308 f2 := MinimalRepoListEntryFromProto(p1)
309 if diff := cmp.Diff(f1, f2); diff != "" {
310 fmt.Printf("got diff: %s", diff)
311 return false
312 }
313 return true
314 }
315 if err := quick.Check(f, nil); err != nil {
316 t.Fatal(err)
317 }
318 })
319
320 t.Run("ListOptions", func(t *testing.T) {
321 f := func(f1 *ListOptions) bool {
322 p1 := f1.ToProto()
323 f2 := ListOptionsFromProto(p1)
324 if diff := cmp.Diff(f1, f2); diff != "" {
325 fmt.Printf("got diff: %s", diff)
326 return false
327 }
328 return true
329 }
330 if err := quick.Check(f, nil); err != nil {
331 t.Fatal(err)
332 }
333 })
334
335 t.Run("SearchOptions", func(t *testing.T) {
336 f := func(f1 *SearchOptions) bool {
337 if f1 != nil {
338 f1.SpanContext = nil
339 }
340 p1 := f1.ToProto()
341 f2 := SearchOptionsFromProto(p1)
342 if diff := cmp.Diff(f1, f2); diff != "" {
343 fmt.Printf("got diff: %s", diff)
344 return false
345 }
346 return true
347 }
348 if err := quick.Check(f, nil); err != nil {
349 t.Fatal(err)
350 }
351 })
352}
353
354func (*IndexMetadata) Generate(r *rand.Rand, _ int) reflect.Value {
355 indexTime := time.Now().Add(time.Duration(r.Int63n(1000)) * time.Hour)
356 var i IndexMetadata
357 i.IndexFormatVersion = gen(i.IndexFormatVersion, r)
358 i.IndexFeatureVersion = gen(i.IndexFeatureVersion, r)
359 i.IndexMinReaderVersion = gen(i.IndexMinReaderVersion, r)
360 i.IndexTime = indexTime
361 i.PlainASCII = gen(i.PlainASCII, r)
362 i.LanguageMap = gen(i.LanguageMap, r)
363 i.ZoektVersion = gen(i.ZoektVersion, r)
364 i.ID = gen(i.ID, r)
365 return reflect.ValueOf(&i)
366}
367
368func (*Repository) Generate(rng *rand.Rand, _ int) reflect.Value {
369 latestCommitDate := time.Now().Add(time.Duration(rng.Int63n(1000)) * time.Hour)
370 var r Repository
371 v := &Repository{
372 ID: gen(r.ID, rng),
373 Name: gen(r.Name, rng),
374 URL: gen(r.URL, rng),
375 Source: gen(r.Source, rng),
376 Branches: gen(r.Branches, rng),
377 SubRepoMap: map[string]*Repository{},
378 CommitURLTemplate: gen(r.CommitURLTemplate, rng),
379 FileURLTemplate: gen(r.FileURLTemplate, rng),
380 LineFragmentTemplate: gen(r.LineFragmentTemplate, rng),
381 priority: gen(r.priority, rng),
382 RawConfig: gen(r.RawConfig, rng),
383 Rank: gen(r.Rank, rng),
384 IndexOptions: gen(r.IndexOptions, rng),
385 HasSymbols: gen(r.HasSymbols, rng),
386 Tombstone: gen(r.Tombstone, rng),
387 LatestCommitDate: latestCommitDate,
388 FileTombstones: gen(r.FileTombstones, rng),
389 }
390 return reflect.ValueOf(v)
391}
392
393func (RepoListField) Generate(rng *rand.Rand, _ int) reflect.Value {
394 if rng.Intn(2) == 0 {
395 return reflect.ValueOf(RepoListField(RepoListFieldRepos))
396 } else {
397 return reflect.ValueOf(RepoListField(RepoListFieldReposMap))
398 }
399}
400
401func gen[T any](sample T, r *rand.Rand) T {
402 var t T
403 v, _ := quick.Value(reflect.TypeOf(t), r)
404 return v.Interface().(T)
405}
406
407// This is a real search result that is intended to be a reasonable representative
408// for serialization benchmarks.
409// Generated by modifying the code to dump the proto to a file, then running a
410// fairly broadly-matching search.
411var (
412 //go:embed testdata/search_result_1.pb
413 exampleSearchResultBytes []byte
414
415 // The proto struct representation of the search result
416 exampleSearchResultProto = func() *webserverv1.SearchResponse {
417 sr := new(webserverv1.SearchResponse)
418 err := proto.Unmarshal(exampleSearchResultBytes, sr)
419 if err != nil {
420 panic(err)
421 }
422 return sr
423 }()
424
425 // The non-proto struct representation of the search result
426 exampleSearchResultGo = SearchResultFromProto(exampleSearchResultProto, nil, nil)
427)
428
429func BenchmarkGobRoundtrip(b *testing.B) {
430 for _, count := range []int{1, 100, 1000, 10000} {
431 b.Run(fmt.Sprintf("count=%d", count), func(b *testing.B) {
432 for i := 0; i < b.N; i++ {
433 var buf bytes.Buffer
434 enc := gob.NewEncoder(&buf)
435
436 for range count {
437 err := enc.Encode(exampleSearchResultGo)
438 if err != nil {
439 panic(err)
440 }
441
442 }
443
444 dec := gob.NewDecoder(&buf)
445 for range count {
446 var res SearchResult
447 err := dec.Decode(&res)
448 if err != nil {
449 panic(err)
450 }
451 }
452 }
453 })
454 }
455}
456
457func BenchmarkProtoRoundtrip(b *testing.B) {
458 for _, count := range []int{1, 100, 1000, 10000} {
459 b.Run(fmt.Sprintf("count=%d", count), func(b *testing.B) {
460 for i := 0; i < b.N; i++ {
461 buffers := make([][]byte, 0, count)
462 for range count {
463 buf, err := proto.Marshal(exampleSearchResultProto)
464 if err != nil {
465 b.Fatal(err)
466 }
467 buffers = append(buffers, buf)
468 }
469
470 for _, buf := range buffers {
471 res := new(webserverv1.SearchResponse)
472 err := proto.Unmarshal(buf, res)
473 if err != nil {
474 b.Fatal(err)
475 }
476 }
477 }
478 })
479 }
480}
481
482func Fuzz_RepoList_ProtoRoundTrip(f *testing.F) {
483 f.Fuzz(func(t *testing.T, data []byte) {
484 fc := fuzz.NewConsumer(data)
485 fc.AllowUnexportedFields()
486
487 original := &RepoList{}
488 err := fc.GenerateStruct(original)
489 if err != nil {
490 return
491 }
492
493 p := original.ToProto()
494 converted := RepoListFromProto(p)
495
496 opts := []cmp.Option{
497 cmpopts.IgnoreUnexported(Repository{}),
498 cmpopts.EquateEmpty(),
499 }
500
501 if diff := cmp.Diff(original, converted, opts...); diff != "" {
502 t.Fatalf("unexpected diff (-want +got)\n%s", diff)
503 }
504 })
505}