| // 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) |
| } |
| } |
| } |
| } |