|  | // Copyright 2016 Google Inc. All rights reserved. | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | package shards | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "context" | 
|  | "fmt" | 
|  | "log" | 
|  | "os" | 
|  | "runtime" | 
|  | "testing" | 
|  | "time" | 
|  |  | 
|  | "github.com/google/zoekt" | 
|  | "github.com/google/zoekt/query" | 
|  | ) | 
|  |  | 
|  | type crashSearcher struct{} | 
|  |  | 
|  | func (s *crashSearcher) Search(ctx context.Context, q query.Q, opts *zoekt.SearchOptions) (*zoekt.SearchResult, error) { | 
|  | panic("search") | 
|  | } | 
|  |  | 
|  | func (s *crashSearcher) List(ctx context.Context, q query.Q) (*zoekt.RepoList, error) { | 
|  | panic("list") | 
|  | } | 
|  |  | 
|  | func (s *crashSearcher) Stats() (*zoekt.RepoStats, error) { | 
|  | return &zoekt.RepoStats{}, nil | 
|  | } | 
|  |  | 
|  | func (s *crashSearcher) Close() {} | 
|  |  | 
|  | func (s *crashSearcher) String() string { return "crashSearcher" } | 
|  |  | 
|  | func TestCrashResilience(t *testing.T) { | 
|  | out := &bytes.Buffer{} | 
|  | log.SetOutput(out) | 
|  | defer log.SetOutput(os.Stderr) | 
|  | ss := newShardedSearcher(2) | 
|  | ss.shards = map[string]rankedShard{ | 
|  | "x": rankedShard{Searcher: &crashSearcher{}}, | 
|  | } | 
|  |  | 
|  | q := &query.Substring{Pattern: "hoi"} | 
|  | opts := &zoekt.SearchOptions{} | 
|  | if res, err := ss.Search(context.Background(), q, opts); err != nil { | 
|  | t.Fatalf("Search: %v", err) | 
|  | } else if res.Stats.Crashes != 1 { | 
|  | t.Errorf("got stats %#v, want crashes = 1", res.Stats) | 
|  | } | 
|  |  | 
|  | if res, err := ss.List(context.Background(), q); err != nil { | 
|  | t.Fatalf("List: %v", err) | 
|  | } else if res.Crashes != 1 { | 
|  | t.Errorf("got result %#v, want crashes = 1", res) | 
|  | } | 
|  | } | 
|  |  | 
|  | type rankSearcher struct { | 
|  | rank uint16 | 
|  | } | 
|  |  | 
|  | func (s *rankSearcher) Close() { | 
|  | } | 
|  |  | 
|  | func (s *rankSearcher) String() string { | 
|  | return "" | 
|  | } | 
|  |  | 
|  | func (s *rankSearcher) Search(ctx context.Context, q query.Q, opts *zoekt.SearchOptions) (*zoekt.SearchResult, error) { | 
|  | select { | 
|  | case <-ctx.Done(): | 
|  | return &zoekt.SearchResult{}, nil | 
|  | default: | 
|  | } | 
|  |  | 
|  | // Ugly, but without sleep it's too fast, and we can't | 
|  | // simulate the cutoff. | 
|  | time.Sleep(time.Millisecond) | 
|  | return &zoekt.SearchResult{ | 
|  | Files: []zoekt.FileMatch{ | 
|  | { | 
|  | FileName: fmt.Sprintf("f%d", s.rank), | 
|  | Score:    float64(s.rank), | 
|  | }, | 
|  | }, | 
|  | Stats: zoekt.Stats{ | 
|  | MatchCount: 1, | 
|  | }, | 
|  | }, nil | 
|  | } | 
|  |  | 
|  | func (s *rankSearcher) List(ctx context.Context, q query.Q) (*zoekt.RepoList, error) { | 
|  | return &zoekt.RepoList{ | 
|  | Repos: []*zoekt.RepoListEntry{ | 
|  | {Repository: zoekt.Repository{Rank: s.rank}}, | 
|  | }, | 
|  | }, nil | 
|  | } | 
|  |  | 
|  | func TestOrderByShard(t *testing.T) { | 
|  | ss := newShardedSearcher(1) | 
|  |  | 
|  | n := 10 * runtime.GOMAXPROCS(0) | 
|  | for i := 0; i < n; i++ { | 
|  | ss.replace(fmt.Sprintf("shard%d", i), | 
|  | &rankSearcher{ | 
|  | rank: uint16(i), | 
|  | }) | 
|  | } | 
|  |  | 
|  | if res, err := ss.Search(context.Background(), &query.Substring{Pattern: "bla"}, &zoekt.SearchOptions{}); err != nil { | 
|  | t.Errorf("Search: %v", err) | 
|  | } else if len(res.Files) != n { | 
|  | t.Fatalf("empty options: got %d results, want %d", len(res.Files), n) | 
|  | } | 
|  |  | 
|  | opts := zoekt.SearchOptions{ | 
|  | TotalMaxMatchCount: 3, | 
|  | } | 
|  | res, err := ss.Search(context.Background(), &query.Substring{Pattern: "bla"}, &opts) | 
|  | if err != nil { | 
|  | t.Errorf("Search: %v", err) | 
|  | } | 
|  |  | 
|  | if len(res.Files) < opts.TotalMaxMatchCount { | 
|  | t.Errorf("got %d results, want %d", len(res.Files), opts.TotalMaxMatchCount) | 
|  | } | 
|  | if len(res.Files) == n { | 
|  | t.Errorf("got %d results, want < %d", len(res.Files), n) | 
|  | } | 
|  | for i, f := range res.Files { | 
|  | rev := n - 1 - i | 
|  | want := fmt.Sprintf("f%d", rev) | 
|  | got := f.FileName | 
|  |  | 
|  | if got != want { | 
|  | t.Logf("%d: got %q, want %q", i, got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | type memSeeker struct { | 
|  | data []byte | 
|  | } | 
|  |  | 
|  | func (s *memSeeker) Name() string { | 
|  | return "memseeker" | 
|  | } | 
|  |  | 
|  | func (s *memSeeker) Close() {} | 
|  | func (s *memSeeker) Read(off, sz uint32) ([]byte, error) { | 
|  | return s.data[off : off+sz], nil | 
|  | } | 
|  |  | 
|  | func (s *memSeeker) Size() (uint32, error) { | 
|  | return uint32(len(s.data)), nil | 
|  | } | 
|  |  | 
|  | func TestUnloadIndex(t *testing.T) { | 
|  | b, err := zoekt.NewIndexBuilder(nil) | 
|  | if err != nil { | 
|  | t.Fatalf("NewIndexBuilder: %v", err) | 
|  | } | 
|  |  | 
|  | for i, d := range []zoekt.Document{{ | 
|  | Name:    "filename", | 
|  | Content: []byte("needle needle needle"), | 
|  | }} { | 
|  | if err := b.Add(d); err != nil { | 
|  | t.Fatalf("Add %d: %v", i, err) | 
|  | } | 
|  | } | 
|  |  | 
|  | var buf bytes.Buffer | 
|  | b.Write(&buf) | 
|  | indexBytes := buf.Bytes() | 
|  | indexFile := &memSeeker{indexBytes} | 
|  | searcher, err := zoekt.NewSearcher(indexFile) | 
|  | if err != nil { | 
|  | t.Fatalf("NewSearcher: %v", err) | 
|  | } | 
|  |  | 
|  | ss := newShardedSearcher(2) | 
|  | ss.replace("key", searcher) | 
|  |  | 
|  | var opts zoekt.SearchOptions | 
|  | q := &query.Substring{Pattern: "needle"} | 
|  | res, err := ss.Search(context.Background(), q, &opts) | 
|  | if err != nil { | 
|  | t.Fatalf("Search(%s): %v", q, err) | 
|  | } | 
|  |  | 
|  | forbidden := byte(29) | 
|  | for i := range indexBytes { | 
|  | // non-ASCII | 
|  | indexBytes[i] = forbidden | 
|  | } | 
|  |  | 
|  | for _, f := range res.Files { | 
|  | if bytes.Contains(f.Content, []byte{forbidden}) { | 
|  | t.Errorf("found %d in content %q", forbidden, f.Content) | 
|  | } | 
|  | if bytes.Contains(f.Checksum, []byte{forbidden}) { | 
|  | t.Errorf("found %d in checksum %q", forbidden, f.Checksum) | 
|  | } | 
|  |  | 
|  | for _, l := range f.LineMatches { | 
|  | if bytes.Contains(l.Line, []byte{forbidden}) { | 
|  | t.Errorf("found %d in line %q", forbidden, l.Line) | 
|  | } | 
|  | } | 
|  | } | 
|  | } |