| Index: common/data/treapstore/store_test.go
|
| diff --git a/common/data/treapstore/store_test.go b/common/data/treapstore/store_test.go
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..b5219d92e0f4d87a00a5269ee9fbc89333c04777
|
| --- /dev/null
|
| +++ b/common/data/treapstore/store_test.go
|
| @@ -0,0 +1,180 @@
|
| +// Copyright 2016 The LUCI Authors. All rights reserved.
|
| +// Use of this source code is governed under the Apache License, Version 2.0
|
| +// that can be found in the LICENSE file.
|
| +
|
| +package treapstore
|
| +
|
| +import (
|
| + "fmt"
|
| + "strconv"
|
| + "strings"
|
| + "testing"
|
| +
|
| + "github.com/luci/gtreap"
|
| +
|
| + . "github.com/smartystreets/goconvey/convey"
|
| +)
|
| +
|
| +func newStringStore() *Store {
|
| + return New(func(a, b interface{}) int {
|
| + return strings.Compare(a.(string), b.(string))
|
| + })
|
| +}
|
| +
|
| +func iterAll(it *gtreap.Iterator) []string {
|
| + all := []string{}
|
| + for {
|
| + v, ok := it.Next()
|
| + if !ok {
|
| + return all
|
| + }
|
| + all = append(all, v.(string))
|
| + }
|
| +}
|
| +
|
| +func TestStore(t *testing.T) {
|
| + t.Parallel()
|
| +
|
| + Convey(`Testing a string Store`, t, func() {
|
| + st := newStringStore()
|
| + st.GetOrCreateCollection("")
|
| +
|
| + Convey(`When empty`, func() {
|
| + checkEmpty := func(c *Collection) {
|
| + So(c.Get("foo"), ShouldBeNil)
|
| + So(iterAll(c.Iterator("")), ShouldResemble, []string{})
|
| + }
|
| +
|
| + // Check the basic Store.
|
| + checkEmpty(st.GetCollection(""))
|
| +
|
| + // Take a snapshot, then mutate the base Store. Assert that the snapshot
|
| + // is still empty.
|
| + snap := st.Snapshot()
|
| + st.GetCollection("").Put("foo")
|
| + checkEmpty(snap.GetCollection(""))
|
| + })
|
| +
|
| + Convey(`With keys`, func() {
|
| + coll := st.GetCollection("")
|
| + coll.Put("a")
|
| + coll.Put("b")
|
| + coll.Put("z")
|
| +
|
| + checkKeys := func(coll *Collection, keys ...string) {
|
| + for _, k := range keys {
|
| + So(coll.Get(k), ShouldEqual, k)
|
| + }
|
| +
|
| + So(iterAll(coll.Iterator("")), ShouldResemble, keys)
|
| + for i, k := range keys {
|
| + So(iterAll(coll.Iterator(k)), ShouldResemble, keys[i:])
|
| + So(iterAll(coll.Iterator(k+"1")), ShouldResemble, keys[i+1:])
|
| + }
|
| + }
|
| + checkKeys(coll, "a", "b", "z")
|
| +
|
| + // Take a snapshot, then mutate the base Store. Assert that the snapshot
|
| + // is still empty.
|
| + snap := st.Snapshot()
|
| + snapColl := snap.GetCollection("")
|
| + coll.Put("foo")
|
| + checkKeys(snapColl, "a", "b", "z")
|
| + checkKeys(coll, "a", "b", "foo", "z")
|
| + })
|
| + })
|
| +}
|
| +
|
| +// TestStoreParallel performs several rounds of parallel accesses. Each round
|
| +// takes a snapshot of the "head" Store, then simultaneusly dispatches a round
|
| +// of parallel writes agains the "head" store, reads against the snapshot, and
|
| +// reads against the "head" store.
|
| +//
|
| +// This is meant to be run with "-race" to trigger on race conditions.
|
| +func TestStoreParallel(t *testing.T) {
|
| + t.Parallel()
|
| +
|
| + Convey(`Testing a string Store for parallel access.`, t, func() {
|
| + const (
|
| + readers = 128
|
| + writers = 16
|
| + rounds = 8
|
| + )
|
| +
|
| + head := newStringStore()
|
| + var snaps []*Store
|
| +
|
| + // Dispatch readers.
|
| + doReads := func() int {
|
| + readDoneC := make(chan int, readers)
|
| + for i := 0; i < readers; i++ {
|
| + go func() {
|
| + var (
|
| + doneC = make(chan int, 1+len(snaps))
|
| + )
|
| +
|
| + // "head"
|
| + go func() {
|
| + doneC <- len(iterAll(head.GetCollection("").Iterator("")))
|
| + }()
|
| +
|
| + // "snap"
|
| + for _, snap := range snaps {
|
| + go func(snap *Store) {
|
| + doneC <- len(iterAll(snap.GetCollection("").Iterator("")))
|
| + }(snap)
|
| + }
|
| +
|
| + total := 0
|
| + for i := 0; i < 1+len(snaps); i++ {
|
| + total += <-doneC
|
| + }
|
| + readDoneC <- total
|
| + }()
|
| + }
|
| +
|
| + total := 0
|
| + for i := 0; i < readers; i++ {
|
| + total += <-readDoneC
|
| + }
|
| + return total
|
| + }
|
| +
|
| + // Dispatch writers.
|
| + doWrite := func(base string) {
|
| + writeDoneC := make(chan struct{}, writers)
|
| + for i := 0; i < writers; i++ {
|
| + go func(idx int) {
|
| + head.GetOrCreateCollection("").Put(fmt.Sprintf("%s.%d", base, idx))
|
| + writeDoneC <- struct{}{}
|
| + }(i)
|
| + }
|
| +
|
| + for i := 0; i < writers; i++ {
|
| + <-writeDoneC
|
| + }
|
| + }
|
| +
|
| + // Main loop.
|
| + for i := 0; i < rounds; i++ {
|
| + writeDoneC := make(chan struct{})
|
| + readDoneC := make(chan int)
|
| + go func() {
|
| + doWrite(strconv.Itoa(i))
|
| + close(writeDoneC)
|
| + }()
|
| + if i > 0 {
|
| + // The first round has to actually create the Collection.
|
| + go func() {
|
| + readDoneC <- doReads()
|
| + }()
|
| + }
|
| +
|
| + <-writeDoneC
|
| + if i > 0 {
|
| + So(<-readDoneC, ShouldBeGreaterThan, 0)
|
| + }
|
| + snaps = append(snaps, head.Snapshot())
|
| + }
|
| + })
|
| +}
|
|
|