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 }