github.com/cilium/statedb@v0.3.2/quick_test.go (about) 1 package statedb 2 3 import ( 4 "cmp" 5 "iter" 6 "strings" 7 "testing" 8 "testing/quick" 9 10 "github.com/cilium/statedb/index" 11 "github.com/stretchr/testify/require" 12 ) 13 14 var quickConfig = &quick.Config{ 15 MaxCount: 5000, 16 } 17 18 // Use an object with strings for both primary and secondary 19 // indexing. With non-unique indexes we'll create a composite 20 // key out of them by concatenation and thus we need test that 21 // this does not break iteration order etc. 22 type quickObj struct { 23 A, B string 24 } 25 26 func (q quickObj) getA() string { 27 return q.A 28 } 29 func (q quickObj) getB() string { 30 return q.B 31 } 32 33 var ( 34 aIndex = Index[quickObj, string]{ 35 Name: "a", 36 FromObject: func(t quickObj) index.KeySet { 37 return index.NewKeySet(index.String(t.A)) 38 }, 39 FromKey: index.String, 40 FromString: index.FromString, 41 Unique: true, 42 } 43 44 bIndex = Index[quickObj, string]{ 45 Name: "b", 46 FromObject: func(t quickObj) index.KeySet { 47 return index.NewKeySet(index.String(t.B)) 48 }, 49 FromKey: index.String, 50 FromString: index.FromString, 51 Unique: false, 52 } 53 ) 54 55 func isOrdered[A cmp.Ordered, B any](t *testing.T, it iter.Seq2[A, B]) bool { 56 var prev A 57 for a := range it { 58 if cmp.Compare(a, prev) < 0 { 59 t.Logf("isOrdered: %#v < %#v!", a, prev) 60 return false 61 } 62 prev = a 63 } 64 return true 65 } 66 67 func seqLen[A, B any](it iter.Seq2[A, B]) int { 68 n := 0 69 for range it { 70 n++ 71 } 72 return n 73 } 74 75 func TestDB_Quick(t *testing.T) { 76 table, err := NewTable("test", aIndex, bIndex) 77 require.NoError(t, err, "NewTable") 78 db := New() 79 require.NoError(t, db.RegisterTable(table), "RegisterTable") 80 81 anyTable := AnyTable{table} 82 83 numExpected := 0 84 85 check := func(a, b string) bool { 86 txn := db.WriteTxn(table) 87 _, hadOld, err := table.Insert(txn, quickObj{a, b}) 88 require.NoError(t, err, "Insert") 89 if !hadOld { 90 numExpected++ 91 } 92 if numExpected != table.NumObjects(txn) { 93 t.Logf("wrong object count") 94 return false 95 } 96 97 txn.Commit() 98 rtxn := db.ReadTxn() 99 100 if numExpected != table.NumObjects(rtxn) { 101 t.Logf("wrong object count") 102 return false 103 } 104 105 // 106 // Check queries against the primary index 107 // 108 109 if numExpected != seqLen(Map(table.All(rtxn), quickObj.getA)) { 110 t.Logf("All() via aIndex wrong length") 111 return false 112 } 113 if numExpected != seqLen(Map(table.Prefix(rtxn, aIndex.Query("")), quickObj.getA)) { 114 t.Logf("Prefix() via aIndex wrong length") 115 return false 116 } 117 if numExpected != seqLen(Map(table.LowerBound(rtxn, aIndex.Query("")), quickObj.getA)) { 118 t.Logf("LowerBound() via aIndex wrong length") 119 return false 120 } 121 122 obj, _, found := table.Get(rtxn, aIndex.Query(a)) 123 if !found || obj.A != a { 124 t.Logf("Get() via aIndex") 125 return false 126 } 127 for obj := range table.Prefix(rtxn, aIndex.Query(a)) { 128 if !strings.HasPrefix(obj.A, a) { 129 t.Logf("Prefix() returned object with wrong prefix via aIndex") 130 return false 131 } 132 } 133 anyObjs, err := anyTable.Prefix(rtxn, "a", a) 134 require.NoError(t, err, "AnyTable.Prefix") 135 for anyObj := range anyObjs { 136 obj := anyObj.(quickObj) 137 if !strings.HasPrefix(obj.A, a) { 138 t.Logf("AnyTable.Prefix() returned object with wrong prefix via aIndex") 139 return false 140 } 141 } 142 143 for obj := range table.LowerBound(rtxn, aIndex.Query(a)) { 144 if cmp.Compare(obj.A, a) < 0 { 145 t.Logf("LowerBound() order wrong") 146 return false 147 } 148 } 149 anyObjs, err = anyTable.LowerBound(rtxn, "a", a) 150 require.NoError(t, err, "AnyTable.LowerBound") 151 for anyObj := range anyObjs { 152 obj := anyObj.(quickObj) 153 if cmp.Compare(obj.A, a) < 0 { 154 t.Logf("AnyTable.LowerBound() order wrong") 155 return false 156 } 157 } 158 159 if !isOrdered(t, Map(table.All(rtxn), quickObj.getA)) { 160 t.Logf("All() wrong order") 161 return false 162 } 163 if !isOrdered(t, Map(table.Prefix(rtxn, aIndex.Query("")), quickObj.getA)) { 164 t.Logf("Prefix() via aIndex wrong order") 165 return false 166 } 167 if !isOrdered(t, Map(table.LowerBound(rtxn, aIndex.Query("")), quickObj.getA)) { 168 t.Logf("LowerBound() via aIndex wrong order") 169 return false 170 } 171 172 // 173 // Check against the secondary (non-unique index) 174 // 175 176 // Non-unique indexes return the same number of objects as we've inserted. 177 if numExpected != seqLen(table.Prefix(rtxn, bIndex.Query(""))) { 178 t.Logf("Prefix() via bIndex wrong length") 179 return false 180 } 181 if numExpected != seqLen(table.LowerBound(rtxn, bIndex.Query(""))) { 182 t.Logf("LowerBound() via bIndex wrong length") 183 return false 184 } 185 186 // Get returns the first match, but since the index is non-unique, this might 187 // not be the one that we just inserted. 188 obj, _, found = table.Get(rtxn, bIndex.Query(b)) 189 if !found || obj.B != b { 190 t.Logf("Get() via bIndex not found or wrong B") 191 return false 192 } 193 194 found = false 195 for obj := range table.List(rtxn, bIndex.Query(b)) { 196 if obj.B != b { 197 t.Logf("List() via bIndex wrong B") 198 return false 199 } 200 if obj.A == a { 201 found = true 202 } 203 } 204 if !found { 205 t.Logf("List() via bIndex, object not found") 206 return false 207 } 208 209 visited := map[string]struct{}{} 210 for obj := range table.Prefix(rtxn, bIndex.Query(b)) { 211 if !strings.HasPrefix(obj.B, b) { 212 t.Logf("Prefix() via bIndex has wrong prefix") 213 return false 214 } 215 if _, found := visited[obj.A]; found { 216 t.Logf("Prefix() visited object %q twice", obj.A) 217 return false 218 } 219 visited[obj.A] = struct{}{} 220 } 221 222 anyObjs, err = anyTable.Prefix(rtxn, "b", b) 223 require.NoError(t, err, "AnyTable.Prefix") 224 for anyObj := range anyObjs { 225 obj := anyObj.(quickObj) 226 if !strings.HasPrefix(obj.B, b) { 227 t.Logf("AnyTable.Prefix() via bIndex has wrong prefix") 228 return false 229 } 230 } 231 232 visited = map[string]struct{}{} 233 for obj := range table.LowerBound(rtxn, bIndex.Query(b)) { 234 if cmp.Compare(obj.B, b) < 0 { 235 t.Logf("LowerBound() via bIndex has wrong objects, expected %v >= %v", []byte(obj.B), []byte(b)) 236 return false 237 } 238 if _, found := visited[obj.A]; found { 239 t.Logf("Prefix() visited object %q twice", obj.A) 240 return false 241 } 242 visited[obj.A] = struct{}{} 243 } 244 245 anyObjs, err = anyTable.LowerBound(rtxn, "b", b) 246 require.NoError(t, err, "AnyTable.LowerBound") 247 for anyObj := range anyObjs { 248 obj := anyObj.(quickObj) 249 if cmp.Compare(obj.B, b) < 0 { 250 t.Logf("AnyTable.LowerBound() via bIndex has wrong objects, expected %v >= %v", []byte(obj.B), []byte(b)) 251 return false 252 } 253 } 254 255 // Iterating over the secondary index returns the objects in order 256 // defined by the "B" key. 257 if !isOrdered(t, Map(table.Prefix(rtxn, bIndex.Query("")), quickObj.getB)) { 258 t.Logf("Prefix() via bIndex has wrong order") 259 return false 260 } 261 if !isOrdered(t, Map(table.LowerBound(rtxn, bIndex.Query("")), quickObj.getB)) { 262 t.Logf("LowerBound() via bIndex has wrong order") 263 return false 264 } 265 return true 266 } 267 268 require.NoError(t, quick.Check(check, quickConfig)) 269 }