github.com/creachadair/ffs@v0.17.3/index/index_test.go (about) 1 // Copyright 2021 Michael J. Fromberger. 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 15 package index_test 16 17 import ( 18 "strings" 19 "testing" 20 21 _ "embed" 22 23 "github.com/creachadair/ffs/index" 24 "github.com/creachadair/ffs/index/indexpb" 25 "github.com/google/go-cmp/cmp" 26 "github.com/google/go-cmp/cmp/cmpopts" 27 "google.golang.org/protobuf/proto" 28 ) 29 30 //go:embed testdata/keys.txt 31 var keyData string 32 33 func TestIndex(t *testing.T) { 34 keys := strings.Split(strings.TrimSpace(keyData), "\n") 35 t.Logf("Test key data: %d bytes (%d keys)", len(keyData), len(keys)) 36 37 idx := index.New(len(keys), &index.Options{ 38 FalsePositiveRate: 0.01, 39 }) 40 41 // Add keys at even offsets, skip keys at odd ones. 42 // Thus we expect half the keys to be missing. 43 var numAdded, totalKeyBytes int 44 for i, key := range keys { 45 if i%2 == 0 { 46 idx.Add(key) 47 numAdded++ 48 totalKeyBytes += len(key) 49 } 50 } 51 t.Logf("Added %d keys to the index", numAdded) 52 53 stats := idx.Stats() 54 t.Logf("Index stats: %d keys, %d filter bits (m), %d hash seeds", 55 stats.NumKeys, stats.FilterBits, stats.NumHashes) 56 if stats.NumKeys != numAdded { 57 t.Errorf("Wrong number of keys: got %d, want %d", stats.NumKeys, numAdded) 58 } 59 if n := idx.Len(); n != stats.NumKeys || n != numAdded { 60 t.Errorf("Len: got %d, wanted %d == %d", n, stats.NumKeys, numAdded) 61 } 62 t.Logf("Total indexed key size: %d bytes", totalKeyBytes) 63 64 falses := make(map[bool]int) 65 for i, key := range keys { 66 want := i%2 == 0 67 got := idx.Has(key) 68 if got != want { 69 falses[got]++ 70 } 71 } 72 73 // We expect there to be false positives. 74 t.Logf("False positives: %d (%.2f%%)", falses[true], percent(falses[true], len(keys))) 75 76 // There should be no false negatives. 77 if neg := falses[false]; neg != 0 { 78 t.Errorf("False negatives: %d (%.2f%%)", neg, percent(neg, len(keys))) 79 } 80 81 // Marshal the index into protobuf wire format. 82 pbits, err := proto.Marshal(index.Encode(idx)) 83 if err != nil { 84 t.Errorf("Marshal failed: %v", err) 85 } 86 t.Logf("Encoded index: %d bytes in wire format", len(pbits)) 87 88 // Unmarshal the wire format and make sure it round-trips. 89 var dpb indexpb.Index 90 if err := proto.Unmarshal(pbits, &dpb); err != nil { 91 t.Fatalf("Unmarshal failed: %v", err) 92 } 93 94 opts := []cmp.Option{ 95 cmp.AllowUnexported(index.Index{}), 96 cmpopts.IgnoreFields(index.Index{}, "hash"), // function, non-comparable 97 } 98 99 dec, err := index.Decode(&dpb) 100 if err != nil { 101 t.Fatalf("Decode: unexpected error: %v", err) 102 } 103 if diff := cmp.Diff(dec, idx, opts...); diff != "" { 104 t.Errorf("Decoded index: (-want, +got)\n%s", diff) 105 } 106 } 107 108 func percent(x, n int) float64 { return 100 * (float64(x) / float64(n)) }