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  }