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