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 web
16
17import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "fmt"
22 "io"
23 "log"
24 "net/http"
25 "net/http/httptest"
26 "reflect"
27 "sort"
28 "strings"
29 "testing"
30 "time"
31
32 "github.com/google/go-cmp/cmp"
33 "github.com/sourcegraph/zoekt"
34 "github.com/sourcegraph/zoekt/query"
35 "github.com/sourcegraph/zoekt/rpc"
36 "github.com/sourcegraph/zoekt/stream"
37)
38
39// TODO(hanwen): cut & paste from ../ . Should create internal test
40// util package.
41type memSeeker struct {
42 data []byte
43}
44
45func (s *memSeeker) Close() {}
46func (s *memSeeker) Read(off, sz uint32) ([]byte, error) {
47 return s.data[off : off+sz], nil
48}
49
50func (s *memSeeker) Size() (uint32, error) {
51 return uint32(len(s.data)), nil
52}
53
54func (s *memSeeker) Name() string {
55 return "memSeeker"
56}
57
58func searcherForTest(t *testing.T, b *zoekt.IndexBuilder) zoekt.Streamer {
59 var buf bytes.Buffer
60 if err := b.Write(&buf); err != nil {
61 t.Fatal(err)
62 }
63 f := &memSeeker{buf.Bytes()}
64
65 searcher, err := zoekt.NewSearcher(f)
66 if err != nil {
67 t.Fatalf("NewSearcher: %v", err)
68 }
69
70 return adapter{Searcher: searcher}
71}
72
73type adapter struct {
74 zoekt.Searcher
75}
76
77func (a adapter) StreamSearch(ctx context.Context, q query.Q, opts *zoekt.SearchOptions, sender zoekt.Sender) (err error) {
78 sr, err := a.Searcher.Search(ctx, q, opts)
79 if err != nil {
80 return err
81 }
82 sender.Send(sr)
83 return nil
84}
85
86func TestBasic(t *testing.T) {
87 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
88 Name: "name",
89 URL: "repo-url",
90 CommitURLTemplate: "{{.Version}}",
91 FileURLTemplate: "file-url",
92 LineFragmentTemplate: "#line",
93 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
94 })
95 if err != nil {
96 t.Fatalf("NewIndexBuilder: %v", err)
97 }
98 if err := b.Add(zoekt.Document{
99 Name: "f2",
100 Content: []byte("to carry water in the no later bla"),
101 // --------------0123456789012345678901234567890123
102 // --------------0 1 2 3
103 Branches: []string{"master"},
104 }); err != nil {
105 t.Fatalf("Add: %v", err)
106 }
107
108 s := searcherForTest(t, b)
109 srv := Server{
110 Searcher: s,
111 Top: Top,
112 HTML: true,
113 }
114
115 mux, err := NewMux(&srv)
116 if err != nil {
117 t.Fatalf("NewMux: %v", err)
118 }
119
120 ts := httptest.NewServer(mux)
121 defer ts.Close()
122
123 nowStr := time.Now().UTC().Format("Jan 02, 2006 15:04")
124 for req, needles := range map[string][]string{
125 "/": {"from 1 repositories"},
126 "/search?q=water": {
127 "href=\"file-url#line",
128 "carry <b>water</b>",
129 },
130 "/search?q=r:": {
131 "1234\">master",
132 "Found 1 repositories",
133 nowStr,
134 "repo-url\">name",
135 "1 files (36B)",
136 },
137 "/search?q=magic": {
138 `value=magic`,
139 },
140 "/robots.txt": {
141 "disallow: /search",
142 },
143 } {
144 checkNeedles(t, ts, req, needles)
145 }
146}
147
148func TestPrint(t *testing.T) {
149 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
150 Name: "name",
151 URL: "repo-url",
152 CommitURLTemplate: "{{.Version}}",
153 FileURLTemplate: "file-url",
154 LineFragmentTemplate: "line",
155 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
156 })
157 if err != nil {
158 t.Fatalf("NewIndexBuilder: %v", err)
159 }
160 if err := b.Add(zoekt.Document{
161 Name: "f2",
162 Content: []byte("to carry water in the no later bla"),
163 Branches: []string{"master"},
164 }); err != nil {
165 t.Fatalf("Add: %v", err)
166 }
167
168 if err := b.Add(zoekt.Document{
169 Name: "dir/f2",
170 Content: []byte("blabla"),
171 Branches: []string{"master"},
172 }); err != nil {
173 t.Fatalf("Add: %v", err)
174 }
175
176 s := searcherForTest(t, b)
177 srv := Server{
178 Searcher: s,
179 Top: Top,
180 HTML: true,
181 Print: true,
182 }
183
184 mux, err := NewMux(&srv)
185 if err != nil {
186 t.Fatalf("NewMux: %v", err)
187 }
188
189 ts := httptest.NewServer(mux)
190 defer ts.Close()
191
192 for req, needles := range map[string][]string{
193 "/print?q=bla&r=name&f=f2": {
194 `pre id="l1" class="inline-pre"><span class="noselect"><a href="#l1">`,
195 },
196 } {
197 checkNeedles(t, ts, req, needles)
198 }
199}
200
201func TestPrintDefault(t *testing.T) {
202 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
203 Name: "name",
204 URL: "repo-url",
205 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
206 })
207 if err != nil {
208 t.Fatalf("NewIndexBuilder: %v", err)
209 }
210 if err := b.Add(zoekt.Document{
211 Name: "f2",
212 Content: []byte("to carry water in the no later bla"),
213 Branches: []string{"master"},
214 }); err != nil {
215 t.Fatalf("Add: %v", err)
216 }
217 s := searcherForTest(t, b)
218 srv := Server{
219 Searcher: s,
220 Top: Top,
221 HTML: true,
222 }
223
224 mux, err := NewMux(&srv)
225 if err != nil {
226 t.Fatalf("NewMux: %v", err)
227 }
228
229 ts := httptest.NewServer(mux)
230 defer ts.Close()
231
232 for req, needles := range map[string][]string{
233 "/search?q=water": {
234 `href="print?`,
235 },
236 } {
237 checkNeedles(t, ts, req, needles)
238 }
239}
240
241func checkNeedles(t *testing.T, ts *httptest.Server, req string, needles []string) {
242 res, err := http.Get(ts.URL + req)
243 if err != nil {
244 t.Fatal(err)
245 }
246 resultBytes, err := io.ReadAll(res.Body)
247 res.Body.Close()
248 if err != nil {
249 log.Fatal(err)
250 }
251
252 result := string(resultBytes)
253 for _, want := range needles {
254 if !strings.Contains(result, want) {
255 t.Errorf("query %q: result did not have %q: %s", req, want, result)
256 }
257 }
258 if notWant := "crashed"; strings.Contains(result, notWant) {
259 t.Errorf("result has %q: %s", notWant, result)
260 }
261 if notWant := "bytes skipped)..."; strings.Contains(result, notWant) {
262 t.Errorf("result has %q: %s", notWant, result)
263 }
264}
265
266type Expectation struct {
267 title string
268 fileMatch FileMatch
269}
270
271func TestFormatJson(t *testing.T) {
272 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
273 Name: "name",
274 URL: "repo-url",
275 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
276 })
277 if err != nil {
278 t.Fatalf("NewIndexBuilder: %v", err)
279 }
280 if err := b.Add(zoekt.Document{
281 Name: "f2",
282 Content: []byte("to carry water in the no later bla"),
283 Branches: []string{"master"},
284 }); err != nil {
285 t.Fatalf("Add: %v", err)
286 }
287 s := searcherForTest(t, b)
288 srv := Server{
289 Searcher: s,
290 Top: Top,
291 HTML: true,
292 }
293
294 mux, err := NewMux(&srv)
295 if err != nil {
296 t.Fatalf("NewMux: %v", err)
297 }
298
299 ts := httptest.NewServer(mux)
300 defer ts.Close()
301
302 expected := Expectation{
303 "json basic test",
304 FileMatch{
305 FileName: "f2",
306 Repo: "name",
307 Matches: []Match{
308 {
309 FileName: "f2",
310 LineNum: 1,
311 Fragments: []Fragment{
312 {
313 Pre: "to carry ",
314 Match: "water",
315 Post: " in the no later bla",
316 },
317 },
318 },
319 },
320 },
321 }
322
323 checkResultMatches(t, ts, "/search?q=water&format=json", expected)
324}
325
326func TestContextLines(t *testing.T) {
327 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
328 Name: "name",
329 URL: "repo-url",
330 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
331 })
332 if err != nil {
333 t.Fatalf("NewIndexBuilder: %v", err)
334 }
335 if err := b.Add(zoekt.Document{
336 Name: "f2",
337 Content: []byte("one line\nsecond snippet\nthird thing\nfourth\nfifth block\nsixth example\nseventh"),
338 Branches: []string{"master"},
339 }); err != nil {
340 t.Fatalf("Add: %v", err)
341 }
342 if err := b.Add(zoekt.Document{
343 Name: "f3",
344 Content: []byte("\n\n\n\nto carry water in the no later bla\n\n\n\n"),
345 Branches: []string{"master"},
346 }); err != nil {
347 t.Fatalf("Add: %v", err)
348 }
349 if err := b.Add(zoekt.Document{
350 Name: "f4",
351 Content: []byte("un \n \n\ttrois\n \n\nsix\n "),
352 Branches: []string{"master"},
353 }); err != nil {
354 t.Fatalf("Add: %v", err)
355 }
356 if err := b.Add(zoekt.Document{
357 Name: "f5",
358 Content: []byte("\ngreen\npastures\n\nhere"),
359 Branches: []string{"master"},
360 }); err != nil {
361 t.Fatalf("Add: %v", err)
362 }
363 s := searcherForTest(t, b)
364 srv := Server{
365 Searcher: s,
366 Top: Top,
367 HTML: true,
368 }
369
370 mux, err := NewMux(&srv)
371 if err != nil {
372 t.Fatalf("NewMux: %v", err)
373 }
374
375 ts := httptest.NewServer(mux)
376 defer ts.Close()
377
378 for req, expected := range map[string]Expectation{
379 "/search?q=our&format=json&ctx=0": {
380 "no context doesn't return Before or After",
381 FileMatch{
382 FileName: "f2",
383 Repo: "name",
384 Matches: []Match{
385 {
386 FileName: "f2",
387 LineNum: 4,
388 Fragments: []Fragment{
389 {
390 Pre: "f",
391 Match: "our",
392 Post: "th",
393 },
394 },
395 },
396 },
397 },
398 },
399 "/search?q=f:f2&format=json&ctx=2": {
400 "filename does not return Before or After",
401 FileMatch{
402 FileName: "f2",
403 Repo: "name",
404 Matches: []Match{
405 {
406 FileName: "f2",
407 LineNum: 0,
408 Fragments: []Fragment{
409 {
410 Match: "f2",
411 },
412 },
413 },
414 },
415 },
416 },
417 "/search?q=our&format=json&ctx=2": {
418 "context returns Before and After",
419 FileMatch{
420 FileName: "f2",
421 Repo: "name",
422 Matches: []Match{
423 {
424 FileName: "f2",
425 LineNum: 4,
426 Fragments: []Fragment{
427 {
428 Pre: "f",
429 Match: "our",
430 Post: "th",
431 },
432 },
433 Before: "second snippet\nthird thing",
434 After: "fifth block\nsixth example",
435 },
436 },
437 },
438 },
439 "/search?q=one&format=json&ctx=2": {
440 "match at start returns After but no Before",
441 FileMatch{
442 FileName: "f2",
443 Repo: "name",
444 Matches: []Match{
445 {
446 FileName: "f2",
447 LineNum: 1,
448 Fragments: []Fragment{
449 {
450 Pre: "",
451 Match: "one",
452 Post: " line",
453 },
454 },
455 After: "second snippet\nthird thing",
456 },
457 },
458 },
459 },
460 "/search?q=seventh&format=json&ctx=2": {
461 "match at end returns Before but no After",
462 FileMatch{
463 FileName: "f2",
464 Repo: "name",
465 Matches: []Match{
466 {
467 FileName: "f2",
468 LineNum: 7,
469 Fragments: []Fragment{
470 {
471 Pre: "",
472 Match: "seventh",
473 Post: "",
474 },
475 },
476 Before: "fifth block\nsixth example",
477 },
478 },
479 },
480 },
481 "/search?q=seventh&format=json&ctx=10": {
482 "match with large context at end returns whole document",
483 FileMatch{
484 FileName: "f2",
485 Repo: "name",
486 Matches: []Match{
487 {
488 FileName: "f2",
489 LineNum: 7,
490 Fragments: []Fragment{
491 {
492 Pre: "",
493 Match: "seventh",
494 Post: "",
495 },
496 },
497 Before: "one line\nsecond snippet\nthird thing\nfourth\nfifth block\nsixth example",
498 },
499 },
500 },
501 },
502 "/search?q=one&format=json&ctx=10": {
503 "match with large context at start returns whole document",
504 FileMatch{
505 FileName: "f2",
506 Repo: "name",
507 Matches: []Match{
508 {
509 FileName: "f2",
510 LineNum: 1,
511 Fragments: []Fragment{
512 {
513 Pre: "",
514 Match: "one",
515 Post: " line",
516 },
517 },
518 After: "second snippet\nthird thing\nfourth\nfifth block\nsixth example\nseventh",
519 },
520 },
521 },
522 },
523 "/search?q=trois&format=json&ctx=2": {
524 "context returns whitespaces lines",
525 FileMatch{
526 FileName: "f4",
527 Repo: "name",
528 Matches: []Match{
529 {
530 FileName: "f4",
531 LineNum: 3,
532 Fragments: []Fragment{
533 {
534 Pre: "\t",
535 Match: "trois",
536 },
537 },
538 Before: "un \n ",
539 After: " \n",
540 },
541 },
542 },
543 },
544 "/search?q=water&format=json&ctx=4": {
545 "context returns new lines",
546 FileMatch{
547 FileName: "f3",
548 Repo: "name",
549 Matches: []Match{
550 {
551 FileName: "f3",
552 LineNum: 5,
553 Fragments: []Fragment{
554 {
555 Pre: "to carry ",
556 Match: "water",
557 Post: " in the no later bla",
558 },
559 },
560 // Returns 3 instead of 4 new line characters since we swallow
561 // the last new line in Before, Fragments and After.
562 Before: "\n\n\n",
563 // Returns 2 instead of 3 new line characters since a
564 // trailing newline at the end of the file does not
565 // constitue a new line.
566 After: "\n\n",
567 },
568 },
569 },
570 },
571 "/search?q=pastures&format=json&ctx=1": {
572 "context returns empty end line",
573 FileMatch{
574 FileName: "f5",
575 Repo: "name",
576 Matches: []Match{
577 {
578 FileName: "f5",
579 LineNum: 3,
580 Fragments: []Fragment{
581 {
582 Pre: "",
583 Match: "pastures",
584 },
585 },
586 Before: "green",
587 After: "",
588 },
589 },
590 },
591 },
592 } {
593 checkResultMatches(t, ts, req, expected)
594 }
595}
596
597func matchesPartiallyEqual(a, b []Match) bool {
598 if len(a) != len(b) {
599 return false
600 }
601 for i := range a {
602 if a[i].FileName != b[i].FileName {
603 return false
604 }
605 if a[i].LineNum != b[i].LineNum {
606 return false
607 }
608 if !reflect.DeepEqual(a[i].Before, b[i].Before) {
609 return false
610 }
611 if !reflect.DeepEqual(a[i].After, b[i].After) {
612 return false
613 }
614 if !reflect.DeepEqual(a[i].Fragments, b[i].Fragments) {
615 return false
616 }
617 }
618 return true
619}
620
621func checkResultMatches(t *testing.T, ts *httptest.Server, req string, expected Expectation) {
622 res, err := http.Get(ts.URL + req)
623 if err != nil {
624 t.Fatal(err)
625 }
626 resultBytes, err := io.ReadAll(res.Body)
627 res.Body.Close()
628 if err != nil {
629 log.Fatal(err)
630 }
631
632 var result ApiSearchResult
633 if err := json.Unmarshal(resultBytes, &result); err != nil {
634 log.Fatal(err)
635 }
636
637 if len(result.Result.FileMatches) != 1 {
638 t.Fatalf("Expected search to return just one result but it was %d", len(result.Result.FileMatches))
639 }
640 match := result.Result.FileMatches[0]
641 if match.FileName == expected.fileMatch.FileName && match.Repo == expected.fileMatch.Repo {
642 if matchesPartiallyEqual(match.Matches, expected.fileMatch.Matches) {
643 return
644 }
645 }
646
647 t.Errorf(
648 "result doesn't match case <%s>:\nDiff:\n %v",
649 expected.title,
650 cmp.Diff(expected.fileMatch.Matches, result.Result.FileMatches[0].Matches))
651}
652
653func TestContextLinesMustBeValid(t *testing.T) {
654 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
655 Name: "name",
656 URL: "repo-url",
657 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
658 })
659 if err != nil {
660 t.Fatalf("NewIndexBuilder: %v", err)
661 }
662 if err := b.Add(zoekt.Document{
663 Name: "f2",
664 Content: []byte("to carry water in the no later bla"),
665 Branches: []string{"master"},
666 }); err != nil {
667 t.Fatalf("Add: %v", err)
668 }
669 s := searcherForTest(t, b)
670 srv := Server{
671 Searcher: s,
672 Top: Top,
673 HTML: true,
674 }
675
676 mux, err := NewMux(&srv)
677 if err != nil {
678 t.Fatalf("NewMux: %v", err)
679 }
680
681 ts := httptest.NewServer(mux)
682 defer ts.Close()
683
684 // Don't care about ctx if format is not json
685 code := getHttpStatusCode(t, ts, "/search?q=water&ctx=10")
686 if code != 200 {
687 t.Errorf("Expected 200 but got %v", code)
688 }
689
690 // ctx must be a valid integer in the right range.
691 for _, want := range []string{"foo", "-1", "20"} {
692 code := getHttpStatusCode(t, ts, "/search?q=water&format=json&ctx="+want)
693 if code != 418 {
694 t.Errorf("Expected 418 but got %v", code)
695 }
696 }
697}
698
699func getHttpStatusCode(t *testing.T, ts *httptest.Server, req string) int {
700 res, err := http.Get(ts.URL + req)
701 if err != nil {
702 t.Fatal(err)
703 }
704 return res.StatusCode
705}
706
707type crashSearcher struct {
708 zoekt.Streamer
709}
710
711func (s *crashSearcher) Search(ctx context.Context, q query.Q, opts *zoekt.SearchOptions) (*zoekt.SearchResult, error) {
712 res := zoekt.SearchResult{}
713 res.Stats.Crashes = 1
714 return &res, nil
715}
716
717func TestCrash(t *testing.T) {
718 srv := Server{
719 Searcher: &crashSearcher{},
720 Top: Top,
721 HTML: true,
722 }
723
724 mux, err := NewMux(&srv)
725 if err != nil {
726 t.Fatalf("NewMux: %v", err)
727 }
728
729 ts := httptest.NewServer(mux)
730 defer ts.Close()
731
732 res, err := http.Get(ts.URL + "/search?q=water")
733 if err != nil {
734 t.Fatal(err)
735 }
736 resultBytes, err := io.ReadAll(res.Body)
737 res.Body.Close()
738 if err != nil {
739 t.Fatal(err)
740 }
741
742 result := string(resultBytes)
743 if want := "1 shards crashed"; !strings.Contains(result, want) {
744 t.Errorf("result did not have %q: %s", want, result)
745 }
746}
747
748func TestHostCustomization(t *testing.T) {
749 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
750 Name: "name",
751 })
752 if err != nil {
753 t.Fatalf("NewIndexBuilder: %v", err)
754 }
755 if err := b.Add(zoekt.Document{
756 Name: "file",
757 Content: []byte("bla"),
758 }); err != nil {
759 t.Fatalf("Add: %v", err)
760 }
761
762 s := searcherForTest(t, b)
763 srv := Server{
764 Searcher: s,
765 Top: Top,
766 HTML: true,
767 HostCustomQueries: map[string]string{
768 "myproject.io": "r:myproject",
769 },
770 }
771
772 mux, err := NewMux(&srv)
773 if err != nil {
774 t.Fatalf("NewMux: %v", err)
775 }
776
777 ts := httptest.NewServer(mux)
778 defer ts.Close()
779
780 req, err := http.NewRequest("GET", ts.URL, &bytes.Buffer{})
781 if err != nil {
782 t.Fatalf("NewRequest: %v", err)
783 }
784 req.Host = "myproject.io"
785 res, err := (&http.Client{}).Do(req)
786 if err != nil {
787 t.Fatalf("Do(%v): %v", req, err)
788 }
789 resultBytes, err := io.ReadAll(res.Body)
790 res.Body.Close()
791 if err != nil {
792 t.Fatalf("ReadAll: %v", err)
793 }
794
795 if got, want := string(resultBytes), "r:myproject"; !strings.Contains(got, want) {
796 t.Fatalf("got %s, want substring %q", got, want)
797 }
798}
799
800func TestDupResult(t *testing.T) {
801 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
802 Name: "name",
803 })
804 if err != nil {
805 t.Fatalf("NewIndexBuilder: %v", err)
806 }
807
808 for i := 0; i < 2; i++ {
809 if err := b.Add(zoekt.Document{
810 Name: fmt.Sprintf("file%d", i),
811 Content: []byte("bla"),
812 }); err != nil {
813 t.Fatalf("Add: %v", err)
814 }
815 }
816 s := searcherForTest(t, b)
817 srv := Server{
818 Searcher: s,
819 Top: Top,
820 HTML: true,
821 }
822
823 mux, err := NewMux(&srv)
824 if err != nil {
825 t.Fatalf("NewMux: %v", err)
826 }
827
828 ts := httptest.NewServer(mux)
829 defer ts.Close()
830
831 req, err := http.NewRequest("GET", ts.URL+"/search?q=bla", &bytes.Buffer{})
832 if err != nil {
833 t.Fatalf("NewRequest: %v", err)
834 }
835 res, err := (&http.Client{}).Do(req)
836 if err != nil {
837 t.Fatalf("Do(%v): %v", req, err)
838 }
839 resultBytes, err := io.ReadAll(res.Body)
840 res.Body.Close()
841 if err != nil {
842 t.Fatalf("ReadAll: %v", err)
843 }
844
845 if got, want := string(resultBytes), "Duplicate result"; !strings.Contains(got, want) {
846 t.Fatalf("got %s, want substring %q", got, want)
847 }
848}
849
850func TestTruncateLine(t *testing.T) {
851 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
852 Name: "name",
853 })
854 if err != nil {
855 t.Fatalf("NewIndexBuilder: %v", err)
856 }
857
858 largePadding := bytes.Repeat([]byte{'a'}, 100*1000) // 100kb
859 if err := b.Add(zoekt.Document{
860 Name: "file",
861 Content: append(append(largePadding, []byte("helloworld")...), largePadding...),
862 }); err != nil {
863 t.Fatalf("Add: %v", err)
864 }
865 s := searcherForTest(t, b)
866 srv := Server{
867 Searcher: s,
868 Top: Top,
869 HTML: true,
870 }
871
872 mux, err := NewMux(&srv)
873 if err != nil {
874 t.Fatalf("NewMux: %v", err)
875 }
876
877 ts := httptest.NewServer(mux)
878 defer ts.Close()
879
880 req, err := http.NewRequest("GET", ts.URL+"/search?q=helloworld", &bytes.Buffer{})
881 if err != nil {
882 t.Fatalf("NewRequest: %v", err)
883 }
884 res, err := (&http.Client{}).Do(req)
885 if err != nil {
886 t.Fatalf("Do(%v): %v", req, err)
887 }
888 resultBytes, err := io.ReadAll(res.Body)
889 res.Body.Close()
890 if err != nil {
891 t.Fatalf("ReadAll: %v", err)
892 }
893
894 if got, want := len(resultBytes)/1000, 10; got > want {
895 t.Fatalf("got %dkb response, want <= %dkb", got, want)
896 }
897 result := string(resultBytes)
898 if want := "aa<b>helloworld</b>aa"; !strings.Contains(result, want) {
899 t.Fatalf("got %s, want substring %q", result, want)
900 }
901 if want := "bytes skipped)..."; !strings.Contains(result, want) {
902 t.Fatalf("got %s, want substring %q", result, want)
903 }
904}
905
906func TestHealthz(t *testing.T) {
907 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
908 Name: "name",
909 })
910 if err != nil {
911 t.Fatalf("NewIndexBuilder: %v", err)
912 }
913
914 for i := 0; i < 2; i++ {
915 if err := b.Add(zoekt.Document{
916 Name: fmt.Sprintf("file%d", i),
917 Content: []byte("bla"),
918 }); err != nil {
919 t.Fatalf("Add: %v", err)
920 }
921 }
922 s := searcherForTest(t, b)
923 srv := Server{
924 Searcher: s,
925 Top: Top,
926 HTML: true,
927 }
928
929 mux, err := NewMux(&srv)
930 if err != nil {
931 t.Fatalf("NewMux: %v", err)
932 }
933
934 ts := httptest.NewServer(mux)
935 t.Cleanup(ts.Close)
936
937 req, err := http.NewRequest("GET", ts.URL+"/healthz", nil)
938 if err != nil {
939 t.Fatalf("NewRequest: %v", err)
940 }
941 res, err := http.DefaultClient.Do(req)
942 if err != nil {
943 t.Fatalf("Do(%v): %v", req, err)
944 }
945
946 t.Cleanup(func() {
947 res.Body.Close()
948 })
949
950 if res.StatusCode != http.StatusOK {
951 t.Fatalf("want 200 status code, got: %v", res.StatusCode)
952 }
953
954 var result zoekt.SearchResult
955 err = json.NewDecoder(res.Body).Decode(&result)
956 if err != nil {
957 t.Fatalf("json.Decode: %v", err)
958 }
959
960 if reflect.DeepEqual(result, zoekt.SearchResult{}) {
961 t.Fatal("empty result in response")
962 }
963}
964
965func TestRPC(t *testing.T) {
966 b, err := zoekt.NewIndexBuilder(&zoekt.Repository{
967 Name: "name",
968 URL: "repo-url",
969 CommitURLTemplate: "{{.Version}}",
970 FileURLTemplate: "file-url",
971 LineFragmentTemplate: "#line",
972 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
973 })
974 if err != nil {
975 t.Fatalf("NewIndexBuilder: %v", err)
976 }
977 if err := b.Add(zoekt.Document{
978 Name: "f2",
979 Content: []byte("to carry water in the no later bla"),
980 // --------------0123456789012345678901234567890123
981 // --------------0 1 2 3
982 Branches: []string{"master"},
983 }); err != nil {
984 t.Fatalf("Add: %v", err)
985 }
986
987 s := searcherForTest(t, b)
988 srv := Server{
989 Searcher: s,
990 RPC: true,
991 Top: Top,
992 }
993
994 mux, err := NewMux(&srv)
995 if err != nil {
996 t.Fatalf("NewMux: %v", err)
997 }
998
999 ts := httptest.NewServer(mux)
1000 defer ts.Close()
1001
1002 endpoint := ts.Listener.Addr().String()
1003
1004 client := stream.NewClient("http://"+endpoint, nil).WithSearcher(rpc.Client(endpoint))
1005
1006 ctx := context.Background()
1007 q := &query.Substring{Pattern: "water"}
1008 opts := &zoekt.SearchOptions{ChunkMatches: true}
1009 opts.SetDefaults()
1010 results, err := client.Search(ctx, q, opts)
1011 if err != nil {
1012 t.Fatal(err)
1013 }
1014
1015 assertResults(t, results.Files, "f2: to carry water in the no later bla")
1016
1017 // TODO grpc, List, StreamSearch
1018}
1019
1020func assertResults(t *testing.T, files []zoekt.FileMatch, want string) {
1021 t.Helper()
1022
1023 var lines []string
1024 for _, fm := range files {
1025 for _, cm := range fm.ChunkMatches {
1026 lines = append(lines, fmt.Sprintf("%s: %s", fm.FileName, string(cm.Content)))
1027 }
1028 }
1029 sort.Strings(lines)
1030 got := strings.TrimSpace(strings.Join(lines, "\n"))
1031 want = strings.TrimSpace(want)
1032
1033 if d := cmp.Diff(want, got); d != "" {
1034 t.Fatalf("unexpected results (-want, +got):\n%s", d)
1035 }
1036}