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