go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/treapstore/store_test.go (about) 1 // Copyright 2016 The LUCI Authors. 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 treapstore 16 17 import ( 18 "fmt" 19 "strconv" 20 "strings" 21 "testing" 22 23 "github.com/luci/gtreap" 24 25 . "github.com/smartystreets/goconvey/convey" 26 ) 27 28 func stringCompare(a, b any) int { 29 return strings.Compare(a.(string), b.(string)) 30 } 31 32 func putMulti(c *Collection, vs ...string) { 33 for _, v := range vs { 34 c.Put(v) 35 } 36 } 37 38 func visitAll(c *Collection, pivot string) []string { 39 res := []string{} 40 c.VisitAscend(pivot, func(v gtreap.Item) bool { 41 res = append(res, v.(string)) 42 return true 43 }) 44 return res 45 } 46 47 func iterAll(it *gtreap.Iterator) []string { 48 all := []string{} 49 for { 50 v, ok := it.Next() 51 if !ok { 52 return all 53 } 54 all = append(all, v.(string)) 55 } 56 } 57 58 func shouldHaveKeys(actual any, expected ...any) string { 59 c := actual.(*Collection) 60 61 // expected can either be a single []string or a series of strings. 62 var keys []string 63 var ok bool 64 if len(expected) == 1 { 65 keys, ok = expected[0].([]string) 66 } 67 if !ok { 68 keys = make([]string, len(expected)) 69 for i, v := range expected { 70 keys[i] = v.(string) 71 } 72 } 73 74 if err := ShouldResemble(iterAll(c.Iterator("")), keys); err != "" { 75 return fmt.Sprintf("failed via iterator: %s", err) 76 } 77 if err := ShouldResemble(visitAll(c, ""), keys); err != "" { 78 return fmt.Sprintf("failed via visit: %s", err) 79 } 80 return "" 81 } 82 83 func TestStore(t *testing.T) { 84 t.Parallel() 85 86 Convey(`Testing a string Store`, t, func() { 87 st := New() 88 coll := st.CreateCollection("test", stringCompare) 89 90 Convey(`When empty`, func() { 91 checkEmpty := func(c *Collection) { 92 So(c.Get("foo"), ShouldBeNil) 93 So(c, shouldHaveKeys) 94 } 95 96 // Check the basic Store. 97 checkEmpty(coll) 98 99 // Take a snapshot, then mutate the base Store. Assert that the snapshot 100 // is still empty. 101 snap := st.Snapshot() 102 coll.Put("foo") 103 checkEmpty(snap.GetCollection("test")) 104 }) 105 106 Convey(`With keys`, func() { 107 putMulti(coll, "x", "w", "b", "a") 108 109 Convey(`Can iterate`, func() { 110 checkKeys := func(coll *Collection, keys ...string) { 111 for _, k := range keys { 112 So(coll.Get(k), ShouldEqual, k) 113 } 114 115 So(coll, shouldHaveKeys, keys) 116 for i, k := range keys { 117 So(iterAll(coll.Iterator(k)), ShouldResemble, keys[i:]) 118 So(iterAll(coll.Iterator(k+"1")), ShouldResemble, keys[i+1:]) 119 } 120 } 121 checkKeys(coll, "a", "b", "w", "x") 122 123 // Take a snapshot, then mutate the base Store. Assert that the snapshot 124 // is still empty. 125 snap := st.Snapshot() 126 snapColl := snap.GetCollection("test") 127 putMulti(coll, "foo") 128 coll.Delete("b") 129 checkKeys(snapColl, "a", "b", "w", "x") 130 checkKeys(coll, "a", "foo", "w", "x") 131 }) 132 133 Convey(`Modified after a snapshot`, func() { 134 snap := st.Snapshot() 135 snapColl := snap.GetCollection("test") 136 putMulti(coll, "z") 137 138 Convey(`A snapshot of a snapshot is itself.`, func() { 139 So(snap.Snapshot(), ShouldEqual, snap) 140 }) 141 142 Convey(`A snapshot is read-only, and cannot create collections.`, func() { 143 So(func() { snap.CreateCollection("new", stringCompare) }, ShouldPanic) 144 }) 145 146 Convey(`Can get its collection name`, func() { 147 So(coll.Name(), ShouldEqual, "test") 148 So(snapColl.Name(), ShouldEqual, "test") 149 }) 150 151 Convey(`Can fetch the Min and Max`, func() { 152 So(coll.Min(), ShouldResemble, "a") 153 So(coll.Max(), ShouldResemble, "z") 154 155 So(snapColl.Min(), ShouldResemble, "a") 156 So(snapColl.Max(), ShouldResemble, "x") 157 }) 158 159 Convey(`Cannot Put to a read-only snapshot.`, func() { 160 So(func() { snapColl.Put("panic") }, ShouldPanic) 161 }) 162 }) 163 }) 164 165 Convey(`Creating a Collection with a duplicate name will panic.`, func() { 166 So(func() { st.CreateCollection("test", stringCompare) }, ShouldPanic) 167 }) 168 169 Convey(`With multiple Collections`, func() { 170 for _, v := range []string{"foo", "bar", "baz"} { 171 st.CreateCollection(v, stringCompare) 172 } 173 So(st.GetCollectionNames(), ShouldResemble, []string{"bar", "baz", "foo", "test"}) 174 snap := st.Snapshot() 175 So(snap.GetCollectionNames(), ShouldResemble, []string{"bar", "baz", "foo", "test"}) 176 177 Convey(`When new Collections are added, names remain sorted.`, func() { 178 for _, v := range []string{"app", "cat", "bas", "qux"} { 179 st.CreateCollection(v, stringCompare) 180 } 181 So(st.GetCollectionNames(), ShouldResemble, 182 []string{"app", "bar", "bas", "baz", "cat", "foo", "qux", "test"}) 183 So(st.Snapshot().GetCollectionNames(), ShouldResemble, 184 []string{"app", "bar", "bas", "baz", "cat", "foo", "qux", "test"}) 185 So(snap.GetCollectionNames(), ShouldResemble, []string{"bar", "baz", "foo", "test"}) 186 }) 187 }) 188 }) 189 } 190 191 func TestStoreZeroValue(t *testing.T) { 192 t.Parallel() 193 194 Convey(`A Store's zero value is valid, empty, and read-only.`, t, func() { 195 s := Store{} 196 197 So(s.IsReadOnly(), ShouldBeTrue) 198 So(s.GetCollectionNames(), ShouldBeNil) 199 So(s.GetCollection("foo"), ShouldBeNil) 200 So(s.Snapshot(), ShouldEqual, &s) 201 }) 202 } 203 204 func TestCollectionZeroValue(t *testing.T) { 205 t.Parallel() 206 207 Convey(`A Collection's zero value is valid, empty, and read-only.`, t, func() { 208 c := Collection{} 209 210 So(c.IsReadOnly(), ShouldBeTrue) 211 So(c.Name(), ShouldEqual, "") 212 So(c.Get("foo"), ShouldBeNil) 213 So(c.Min(), ShouldBeNil) 214 So(c.Max(), ShouldBeNil) 215 216 it := c.Iterator(nil) 217 So(it, ShouldNotBeNil) 218 So(iterAll(it), ShouldHaveLength, 0) 219 }) 220 } 221 222 // TestStoreParallel performs several rounds of parallel accesses. Each round 223 // takes a snapshot of the "head" Store, then simultaneusly dispatches a round 224 // of parallel writes against the "head" store, reads against the snapshot, and 225 // reads against the "head" store. 226 // 227 // This is meant to be run with "-race" to trigger on race conditions. 228 func TestStoreParallel(t *testing.T) { 229 t.Parallel() 230 231 Convey(`Testing a string Store for parallel access.`, t, func() { 232 const ( 233 readers = 128 234 writers = 16 235 rounds = 8 236 ) 237 238 head := New() 239 head.CreateCollection("", stringCompare) 240 var snaps []*Store 241 242 // Dispatch readers. 243 doReads := func() int { 244 readDoneC := make(chan int, readers) 245 for i := 0; i < readers; i++ { 246 go func() { 247 var ( 248 doneC = make(chan int, 1+len(snaps)) 249 ) 250 251 // "head" 252 go func() { 253 doneC <- len(iterAll(head.GetCollection("").Iterator(""))) 254 }() 255 256 // "snap" 257 for _, snap := range snaps { 258 go func(snap *Store) { 259 doneC <- len(iterAll(snap.GetCollection("").Iterator(""))) 260 }(snap) 261 } 262 263 total := 0 264 for i := 0; i < 1+len(snaps); i++ { 265 total += <-doneC 266 } 267 readDoneC <- total 268 }() 269 } 270 271 total := 0 272 for i := 0; i < readers; i++ { 273 total += <-readDoneC 274 } 275 return total 276 } 277 278 // Dispatch writers. 279 doWrite := func(base string) { 280 writeDoneC := make(chan struct{}, writers) 281 for i := 0; i < writers; i++ { 282 go func(idx int) { 283 head.GetCollection("").Put(fmt.Sprintf("%s.%d", base, idx)) 284 writeDoneC <- struct{}{} 285 }(i) 286 } 287 288 for i := 0; i < writers; i++ { 289 <-writeDoneC 290 } 291 } 292 293 // Main loop. 294 for i := 0; i < rounds; i++ { 295 writeDoneC := make(chan struct{}) 296 readDoneC := make(chan int) 297 go func() { 298 doWrite(strconv.Itoa(i)) 299 close(writeDoneC) 300 }() 301 // The first round has to actually create the Collection. 302 go func() { 303 readDoneC <- doReads() 304 }() 305 306 <-writeDoneC 307 308 check := ShouldBeGreaterThan 309 if i == 0 { 310 // The first time around, we *could* read before anything has been 311 // written. Every other time, something from the previous round will 312 // have been written. 313 check = ShouldBeGreaterThanOrEqualTo 314 } 315 So(<-readDoneC, check, 0) 316 snaps = append(snaps, head.Snapshot()) 317 } 318 }) 319 }