github.com/haraldrudell/parl@v0.4.176/pmaps/rwmap_test.go (about)

     1  /*
     2  © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  // RWMap is a thread-safe mapping.
     7  package pmaps
     8  
     9  import (
    10  	"context"
    11  	"encoding/base64"
    12  	"math/rand"
    13  	"os"
    14  	"slices"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/haraldrudell/parl/parli"
    19  	"github.com/haraldrudell/parl/perrors"
    20  	"golang.org/x/exp/maps"
    21  )
    22  
    23  func TestRWMap(t *testing.T) {
    24  	var key1, key2 = "key1", "key2"
    25  	var value1, value2 = 1, 2
    26  	// expected map length 1
    27  	var lengthExp = 1
    28  	var mapExp1 = map[string]int{key1: value1}
    29  	var lengthExpRange2 = 1
    30  	var mapExp2 = map[string]int{key1: value1, key2: value2}
    31  	var mapExpValue2 = map[string]int{key1: value2}
    32  	var mapExpKey2 = map[string]int{key2: value2}
    33  	var lengthExp0 = 0
    34  	var keysExp = []string{key1}
    35  	var keysLength, listLength = 1, 1
    36  	var newV = func() (valuep *int) { return &value2 }
    37  	var makeV = func() (value int) { return value2 }
    38  
    39  	var lengthAct, value, zeroValue int
    40  	var hasValue, rangedAll, wasNewKey bool
    41  	var ranger *mapRanger[string, int]
    42  	var mapAct map[string]int
    43  	var keys []string
    44  	var values []int
    45  	var clone parli.ThreadSafeMap[string, int]
    46  	var rwmap2 *RWMap[string, int]
    47  	var putIfTrue = func(value int) (doPut bool) {
    48  		if value != value1 {
    49  			panic(perrors.NewPF("putif bad value"))
    50  		}
    51  		return true
    52  	}
    53  	var putIfFalse = func(value int) (doPut bool) {
    54  		if value != value1 {
    55  			panic(perrors.NewPF("putif bad value"))
    56  		}
    57  		return
    58  	}
    59  
    60  	// Get() Put() Delete() Length() Range()
    61  	// GetOrCreate() PutIf()
    62  	// Clear() Clone() Clone2()
    63  	// List() Keys()
    64  	var rwmap *RWMap[string, int]
    65  	var reset = func() {
    66  		rwmap = NewRWMap2[string, int]()
    67  		rwmap.Put(key1, value1)
    68  	}
    69  	var getMap = func() (mapAct map[string]int) {
    70  		var r = newMapRanger[string, int](true)
    71  		rwmap.Range(r.rangeFunc)
    72  		mapAct = r.M
    73  		return
    74  	}
    75  
    76  	// Length should return length
    77  	reset()
    78  	lengthAct = rwmap.Length()
    79  	if lengthAct != lengthExp {
    80  		t.Errorf("Length %d exp %d", lengthAct, lengthExp)
    81  	}
    82  
    83  	// Get existing key should return value
    84  	reset()
    85  	value, hasValue = rwmap.Get(key1)
    86  	if !hasValue {
    87  		t.Error("Get1 hasValue false")
    88  	}
    89  	if value != value1 {
    90  		t.Errorf("Get1 value %d exp %d", value, value1)
    91  	}
    92  
    93  	// Get non-existing key should return no value
    94  	reset()
    95  	value, hasValue = rwmap.Get(key2)
    96  	if hasValue {
    97  		t.Error("Get2 hasValue true")
    98  	}
    99  	if value != zeroValue {
   100  		t.Errorf("Get2 value %d exp %d", value, zeroValue)
   101  	}
   102  
   103  	// Put new key should grow map
   104  	reset()
   105  	rwmap.Put(key2, value2)
   106  	mapAct = getMap()
   107  	if !maps.Equal(mapAct, mapExp2) {
   108  		t.Errorf("Put1 %v exp %v", mapAct, mapExp2)
   109  	}
   110  
   111  	// Put existing key should change map
   112  	reset()
   113  	rwmap.Put(key1, value2)
   114  	mapAct = getMap()
   115  	if !maps.Equal(mapAct, mapExpValue2) {
   116  		t.Errorf("Put2 %v exp %v", mapAct, mapExpValue2)
   117  	}
   118  
   119  	// Delete existing key should change map
   120  	reset()
   121  	rwmap.Put(key2, value2)
   122  	rwmap.Delete(key1)
   123  	mapAct = getMap()
   124  	if !maps.Equal(mapAct, mapExpKey2) {
   125  		t.Errorf("Delete %v exp %v", mapAct, mapExpKey2)
   126  	}
   127  
   128  	// Range should return entire map
   129  	reset()
   130  	ranger = newMapRanger[string, int](true)
   131  	rangedAll = rwmap.Range(ranger.rangeFunc)
   132  	if !rangedAll {
   133  		t.Error("Range1 rangedAll false")
   134  	}
   135  	if !maps.Equal(ranger.M, mapExp1) {
   136  		t.Errorf("Range1 %v exp %v", ranger.M, mapExp1)
   137  	}
   138  
   139  	// Range can be aborted
   140  	reset()
   141  	rwmap.Put(key2, value2)
   142  	ranger = newMapRanger[string, int](false)
   143  	rangedAll = rwmap.Range(ranger.rangeFunc)
   144  	if rangedAll {
   145  		t.Error("Range2 rangedAll true")
   146  	}
   147  	if len(ranger.M) != lengthExpRange2 {
   148  		t.Errorf("Range2 length %d exp %d", len(ranger.M), lengthExpRange2)
   149  	}
   150  
   151  	// Clear should empty map
   152  	reset()
   153  	rwmap.Clear()
   154  	if le := rwmap.Length(); le != lengthExp0 {
   155  		t.Errorf("Clear length %d exp %d", le, lengthExp0)
   156  	}
   157  
   158  	// Keys should return keys
   159  	reset()
   160  	keys = rwmap.Keys()
   161  	if !slices.Equal(keys, keysExp) {
   162  		t.Errorf("Keys1 %v exp %v", keys, keysExp)
   163  	}
   164  
   165  	// Keys can retrieve partial
   166  	reset()
   167  	rwmap.Put(key2, value2)
   168  	keys = rwmap.Keys(keysLength)
   169  	if len(keys) != keysLength {
   170  		t.Fatalf("Keys2 length %d exp %d", len(keys), keysLength)
   171  	}
   172  	if keys[0] != key1 && keys[0] != key2 {
   173  		t.Error("Keys2 bad keys")
   174  	}
   175  
   176  	// List should return values
   177  	reset()
   178  	values = rwmap.List()
   179  	if !slices.Equal(values, []int{value1}) {
   180  		t.Errorf("List1 %v exp %v", values, []int{value1})
   181  	}
   182  
   183  	// List can retrieve partial
   184  	reset()
   185  	rwmap.Put(key2, value2)
   186  	values = rwmap.List(listLength)
   187  	if len(values) != listLength {
   188  		t.Fatalf("List2 length %d exp %d", len(values), listLength)
   189  	}
   190  	if values[0] != value1 && values[0] != value2 {
   191  		t.Error("List2 bad keys")
   192  	}
   193  
   194  	// Clone should clone
   195  	reset()
   196  	clone = rwmap.Clone()
   197  	if le := clone.Length(); le != lengthExp {
   198  		t.Errorf("Clone length %d exp %d", le, lengthExp)
   199  	}
   200  
   201  	// Clone2 should clone
   202  	reset()
   203  	rwmap2 = rwmap.Clone2()
   204  	ranger = newMapRanger[string, int](true)
   205  	rwmap2.Range(ranger.rangeFunc)
   206  	if !maps.Equal(ranger.M, mapExp1) {
   207  		t.Errorf("Clone2 %v exp %v", ranger.M, mapExp1)
   208  	}
   209  
   210  	// GetOrCreate unknown key should return nil
   211  	reset()
   212  	value, hasValue = rwmap.GetOrCreate(key2, nil, nil)
   213  	if hasValue {
   214  		t.Error("GetOrCreate1 hasValue true")
   215  	}
   216  	if value != zeroValue {
   217  		t.Errorf("GetOrCreate1 value %d exp %d", value, zeroValue)
   218  	}
   219  
   220  	// GetOrCreate known key should return value
   221  	reset()
   222  	value, hasValue = rwmap.GetOrCreate(key1, nil, nil)
   223  	if !hasValue {
   224  		t.Error("GetOrCreate2 hasValue false")
   225  	}
   226  	if value != value1 {
   227  		t.Errorf("GetOrCreate2 value %d exp %d", value, value1)
   228  	}
   229  
   230  	// GetOrCreate should use newV
   231  	reset()
   232  	value, hasValue = rwmap.GetOrCreate(key2, newV, nil)
   233  	if !hasValue {
   234  		t.Error("GetOrCreate3 hasValue false")
   235  	}
   236  	if value != value2 {
   237  		t.Errorf("GetOrCreate3 value %d exp %d", value, value2)
   238  	}
   239  
   240  	// GetOrCreate should use makeV
   241  	reset()
   242  	value, hasValue = rwmap.GetOrCreate(key2, nil, makeV)
   243  	if !hasValue {
   244  		t.Error("GetOrCreate4 hasValue false")
   245  	}
   246  	if value != value2 {
   247  		t.Errorf("GetOrCreate4 value %d exp %d", value, value2)
   248  	}
   249  
   250  	// Putif new key should add mapping
   251  	reset()
   252  	wasNewKey = rwmap.PutIf(key2, value2, nil)
   253  	if !wasNewKey {
   254  		t.Error("PutIf1 wasNewKey false")
   255  	}
   256  	mapAct = getMap()
   257  	if !maps.Equal(mapAct, mapExp2) {
   258  		t.Errorf("PutIf1 map %v exp %v", mapAct, mapExp2)
   259  	}
   260  
   261  	// PutIf false should not update
   262  	reset()
   263  	wasNewKey = rwmap.PutIf(key1, value2, putIfFalse)
   264  	if wasNewKey {
   265  		t.Error("PutIf2 wasNewKey true")
   266  	}
   267  	mapAct = getMap()
   268  	if !maps.Equal(mapAct, mapExp1) {
   269  		t.Errorf("PutIf2 map %v exp %v", mapAct, mapExp1)
   270  	}
   271  
   272  	// PutIf true should update
   273  	reset()
   274  	wasNewKey = rwmap.PutIf(key1, value2, putIfTrue)
   275  	if wasNewKey {
   276  		t.Error("PutIf2 wasNewKey true")
   277  	}
   278  	mapAct = getMap()
   279  	if !maps.Equal(mapAct, mapExpValue2) {
   280  		t.Errorf("PutIf2 map %v exp %v", mapAct, mapExpValue2)
   281  	}
   282  }
   283  
   284  // ITEST= go test -race -v -run '^TestRWMapRace$' ./pmaps
   285  func TestRWMapRace(t *testing.T) {
   286  	randomLength := 16
   287  	limitedSliceSize := 100
   288  	lap := 100
   289  	value := 3
   290  	duration := time.Second
   291  
   292  	// check environment
   293  	if _, ok := os.LookupEnv("ITEST"); !ok {
   294  		t.Skip("ITEST not present")
   295  	}
   296  
   297  	var limitedSlice = make([]string, limitedSliceSize)
   298  	for i := 0; i < limitedSliceSize; i++ {
   299  		limitedSlice[i] = randomAZ(randomLength)
   300  	}
   301  
   302  	var rwMap RWMap[string, int] = *NewRWMap2[string, int]()
   303  	var ctx, cancelFunc = context.WithCancel(context.Background())
   304  	defer cancelFunc()
   305  	rand.Seed(time.Now().UnixNano())
   306  
   307  	// put thread
   308  	go func() {
   309  		for ctx.Err() == nil {
   310  			for _, randomString := range limitedSlice {
   311  				rwMap.Put(randomString, value)
   312  			}
   313  			for i := 0; i < lap; i++ {
   314  				rwMap.Put(randomAZ(randomLength), value)
   315  			}
   316  		}
   317  	}()
   318  
   319  	// get thread
   320  	go func() {
   321  		for ctx.Err() == nil {
   322  			for _, randomString := range limitedSlice {
   323  				rwMap.Get(randomString)
   324  			}
   325  			for i := 0; i < lap; i++ {
   326  				rwMap.Get(randomAZ(randomLength))
   327  			}
   328  		}
   329  	}()
   330  
   331  	time.Sleep(duration)
   332  }
   333  
   334  // randomAZ provides a string of random characters using base64 encoding
   335  //   - characters: a-zA-Z0-9+/
   336  //   - use rand.Seed for randomization
   337  func randomAZ(length int) (s string) {
   338  	if length < 1 {
   339  		return
   340  	}
   341  	// base64 encodes 64 values per character, ie. 6/8 bits as in 3 bytes into 4 bytes
   342  	// 1 random byte provides 4/3 characters, ie factor 3/4, and add 1 due to integer truncation
   343  	p := make([]byte, (length+1)*3/4)
   344  	rand.Read(p)
   345  	s = base64.StdEncoding.EncodeToString(p)
   346  	if len(s) > length {
   347  		s = s[:length]
   348  	}
   349  	return
   350  }
   351  
   352  // mapRanger ranges a map Range method
   353  type mapRanger[K comparable, V any] struct {
   354  	M         map[K]V
   355  	keepGoing bool
   356  }
   357  
   358  // newMapRanger returns a tester for a map’s Range method
   359  func newMapRanger[K comparable, V any](keepGoing bool) (ranger *mapRanger[K, V]) {
   360  	return &mapRanger[K, V]{M: make(map[K]V), keepGoing: keepGoing}
   361  }
   362  
   363  // rangeFunc can be provided to a map’s Range method
   364  func (m *mapRanger[K, V]) rangeFunc(key K, value V) (keepGoing bool) {
   365  	m.M[key] = value
   366  	return m.keepGoing
   367  }