github.com/cilium/statedb@v0.3.2/regression_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package statedb 5 6 import ( 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 "github.com/cilium/statedb/index" 14 ) 15 16 // Test_Regression_29324 tests that Get() on a index.String-based 17 // unique index only returns exact matches. 18 // https://github.com/cilium/cilium/issues/29324 19 func Test_Regression_29324(t *testing.T) { 20 type object struct { 21 ID string 22 Tag string 23 } 24 idIndex := Index[object, string]{ 25 Name: "id", 26 FromObject: func(t object) index.KeySet { 27 return index.NewKeySet(index.String(t.ID)) 28 }, 29 FromKey: index.String, 30 Unique: true, 31 } 32 tagIndex := Index[object, string]{ 33 Name: "tag", 34 FromObject: func(t object) index.KeySet { 35 return index.NewKeySet(index.String(t.Tag)) 36 }, 37 FromKey: index.String, 38 Unique: false, 39 } 40 41 db, _, _ := newTestDB(t) 42 table, err := NewTable("objects", idIndex, tagIndex) 43 require.NoError(t, err) 44 require.NoError(t, db.RegisterTable(table)) 45 46 wtxn := db.WriteTxn(table) 47 table.Insert(wtxn, object{"foo", "aa"}) 48 table.Insert(wtxn, object{"foobar", "aaa"}) 49 table.Insert(wtxn, object{"baz", "aaaa"}) 50 wtxn.Commit() 51 52 // Exact match should only return "foo" 53 txn := db.ReadTxn() 54 iter := table.List(txn, idIndex.Query("foo")) 55 items := Collect(iter) 56 if assert.Len(t, items, 1, "Get(\"foo\") should return one match") { 57 assert.EqualValues(t, "foo", items[0].ID) 58 } 59 60 // Partial match on prefix should not return anything 61 iter = table.List(txn, idIndex.Query("foob")) 62 items = Collect(iter) 63 assert.Len(t, items, 0, "Get(\"foob\") should return nothing") 64 65 // Query on non-unique index should only return exact match 66 iter = table.List(txn, tagIndex.Query("aa")) 67 items = Collect(iter) 68 if assert.Len(t, items, 1, "Get(\"aa\") on tags should return one match") { 69 assert.EqualValues(t, "foo", items[0].ID) 70 } 71 72 // Partial match on prefix should not return anything on non-unique index 73 iter = table.List(txn, idIndex.Query("a")) 74 items = Collect(iter) 75 assert.Len(t, items, 0, "Get(\"a\") should return nothing") 76 } 77 78 // The watch channel returned by Changes() must be a closed one if there 79 // is anything left to iterate over. Otherwise on partial iteration we'll 80 // wait on a watch channel that reflects the changes of a full iteration 81 // and we might be stuck waiting even when there's unprocessed changes. 82 func Test_Regression_Changes_Watch(t *testing.T) { 83 db, table, _ := newTestDB(t) 84 85 wtxn := db.WriteTxn(table) 86 changeIter, err := table.Changes(wtxn) 87 require.NoError(t, err, "Changes") 88 wtxn.Commit() 89 90 n := 0 91 changes, watch := changeIter.Next(db.ReadTxn()) 92 for change := range changes { 93 t.Fatalf("did not expect changes, got: %v", change) 94 } 95 96 // The returned watch channel is closed on the first call to Next() 97 // as there may have been changes to iterate and we want it to be 98 // safe to either partially consume the changes or even block first 99 // on the watch channel and only then consume. 100 select { 101 case <-watch: 102 default: 103 t.Fatalf("Changes() watch channel not closed") 104 } 105 106 // Calling Next() again now will get a proper non-closed watch channel. 107 changes, watch = changeIter.Next(db.ReadTxn()) 108 for change := range changes { 109 t.Fatalf("did not expect changes, got: %v", change) 110 } 111 select { 112 case <-watch: 113 t.Fatalf("Changes() watch channel unexpectedly closed") 114 default: 115 } 116 117 wtxn = db.WriteTxn(table) 118 table.Insert(wtxn, testObject{ID: 1}) 119 table.Insert(wtxn, testObject{ID: 2}) 120 table.Insert(wtxn, testObject{ID: 3}) 121 wtxn.Commit() 122 123 // Observe the objects. 124 select { 125 case <-watch: 126 case <-time.After(time.Second): 127 t.Fatalf("Changes() watch channel not closed after inserts") 128 } 129 130 changes, watch = changeIter.Next(db.ReadTxn()) 131 n = 0 132 for change := range changes { 133 require.False(t, change.Deleted, "not deleted") 134 n++ 135 } 136 require.Equal(t, 3, n, "expected 3 objects") 137 138 // Delete the objects 139 wtxn = db.WriteTxn(table) 140 require.NoError(t, table.DeleteAll(wtxn), "DeleteAll") 141 wtxn.Commit() 142 143 // Partially observe the changes 144 <-watch 145 changes, watch = changeIter.Next(db.ReadTxn()) 146 for change := range changes { 147 require.True(t, change.Deleted, "expected Deleted") 148 break 149 } 150 151 // Calling Next again after partially consuming the iterator 152 // should return a closed watch channel. 153 changes, watch = changeIter.Next(db.ReadTxn()) 154 select { 155 case <-watch: 156 case <-time.After(time.Second): 157 t.Fatalf("Changes() watch channel not closed!") 158 } 159 160 // Consume the rest of the deletions. 161 n = 1 162 for change := range changes { 163 require.True(t, change.Deleted, "expected Deleted") 164 n++ 165 } 166 require.Equal(t, 3, n, "expected 3 deletions") 167 } 168 169 // Prefix and LowerBound searches on non-unique indexes did not properly check 170 // whether the object was a false positive due to matching on the primary key part 171 // of the composite key (<secondary><primary><secondary length>). E.g. if the 172 // composite keys were <a><aa><1> and <aa><b><2> then Prefix("aa") incorrectly 173 // yielded the <a><aa><1> as it matched partially the primary key <aa>. 174 // 175 // Also another issue existed with the ordering of the results due to there being 176 // no separator between <secondary> and <primary> parts of the composite key. 177 // E.g. <a><z><1> and <aa><a><2> were yielded in the incorrect order 178 // <aa><a><2> and <a><z><1>, which implied "aa" < "a"! 179 func Test_Regression_Prefix_NonUnique(t *testing.T) { 180 type object struct { 181 ID string 182 Tag string 183 } 184 idIndex := Index[object, string]{ 185 Name: "id", 186 FromObject: func(t object) index.KeySet { 187 return index.NewKeySet(index.String(t.ID)) 188 }, 189 FromKey: index.String, 190 Unique: true, 191 } 192 tagIndex := Index[object, string]{ 193 Name: "tag", 194 FromObject: func(t object) index.KeySet { 195 return index.NewKeySet(index.String(t.Tag)) 196 }, 197 FromKey: index.String, 198 Unique: false, 199 } 200 201 db, _, _ := newTestDB(t) 202 table, err := NewTable("objects", idIndex, tagIndex) 203 require.NoError(t, err) 204 require.NoError(t, db.RegisterTable(table)) 205 206 wtxn := db.WriteTxn(table) 207 table.Insert(wtxn, object{"aa", "a"}) 208 table.Insert(wtxn, object{"b", "bb"}) 209 table.Insert(wtxn, object{"z", "b"}) 210 wtxn.Commit() 211 212 // The tag index has one object with tag "a", prefix searching 213 // "aa" should return nothing. 214 txn := db.ReadTxn() 215 iter := table.Prefix(txn, tagIndex.Query("aa")) 216 items := Collect(iter) 217 assert.Len(t, items, 0, "Prefix(\"aa\") should return nothing") 218 219 iter = table.Prefix(txn, tagIndex.Query("a")) 220 items = Collect(iter) 221 if assert.Len(t, items, 1, "Prefix(\"a\") on tags should return one match") { 222 assert.EqualValues(t, "aa", items[0].ID) 223 } 224 225 // Check prefix search ordering: should be fully defined by the secondary key. 226 iter = table.Prefix(txn, tagIndex.Query("b")) 227 items = Collect(iter) 228 if assert.Len(t, items, 2, "Prefix(\"b\") on tags should return two matches") { 229 assert.EqualValues(t, "z", items[0].ID) 230 assert.EqualValues(t, "b", items[1].ID) 231 } 232 233 // With LowerBound search on "aa" we should see tags "b" and "bb" (in that order) 234 iter = table.LowerBound(txn, tagIndex.Query("aa")) 235 items = Collect(iter) 236 if assert.Len(t, items, 2, "LowerBound(\"aa\") on tags should return two matches") { 237 assert.EqualValues(t, "z", items[0].ID) 238 assert.EqualValues(t, "b", items[1].ID) 239 } 240 }