src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/persistent/hashmap/hashmap_test.go (about) 1 package hashmap 2 3 import ( 4 "math/rand" 5 "reflect" 6 "strconv" 7 "testing" 8 9 "src.elv.sh/pkg/persistent/hash" 10 ) 11 12 const ( 13 NSequential = 0x1000 14 NCollision = 0x100 15 NRandom = 0x4000 16 NReplace = 0x200 17 18 SmallRandomPass = 0x100 19 NSmallRandom = 0x400 20 SmallRandomHighBound = 0x50 21 SmallRandomLowBound = 0x200 22 23 NArrayNode = 0x100 24 25 NIneffectiveDissoc = 0x200 26 27 N1 = nodeCap + 1 28 N2 = nodeCap*nodeCap + 1 29 N3 = nodeCap*nodeCap*nodeCap + 1 30 ) 31 32 type testKey uint64 33 type anotherTestKey uint32 34 35 func equalFunc(k1, k2 any) bool { 36 switch k1 := k1.(type) { 37 case testKey: 38 t2, ok := k2.(testKey) 39 return ok && k1 == t2 40 case anotherTestKey: 41 return false 42 default: 43 return k1 == k2 44 } 45 } 46 47 func hashFunc(k any) uint32 { 48 switch k := k.(type) { 49 case uint32: 50 return k 51 case string: 52 return hash.String(k) 53 case testKey: 54 // Return the lower 32 bits for testKey. This is intended so that hash 55 // collisions can be easily constructed. 56 return uint32(k & 0xffffffff) 57 case anotherTestKey: 58 return uint32(k) 59 default: 60 return 0 61 } 62 } 63 64 var empty = New(equalFunc, hashFunc) 65 66 type refEntry struct { 67 k testKey 68 v string 69 } 70 71 func hex(i uint64) string { 72 return "0x" + strconv.FormatUint(i, 16) 73 } 74 75 var randomStrings []string 76 77 // getRandomStrings returns a slice of N3 random strings. It builds the slice 78 // once and caches it. If the slice is built for the first time, it stops the 79 // timer of the benchmark. 80 func getRandomStrings(b *testing.B) []string { 81 if randomStrings == nil { 82 b.StopTimer() 83 defer b.StartTimer() 84 randomStrings = make([]string, N3) 85 for i := 0; i < N3; i++ { 86 randomStrings[i] = makeRandomString() 87 } 88 } 89 return randomStrings 90 } 91 92 // makeRandomString builds a random string consisting of n bytes (randomized 93 // between 0 and 99) and each byte is randomized between 0 and 255. The string 94 // need not be valid UTF-8. 95 func makeRandomString() string { 96 bytes := make([]byte, rand.Intn(100)) 97 for i := range bytes { 98 bytes[i] = byte(rand.Intn(256)) 99 } 100 return string(bytes) 101 } 102 103 func TestHashMap(t *testing.T) { 104 var refEntries []refEntry 105 add := func(k testKey, v string) { 106 refEntries = append(refEntries, refEntry{k, v}) 107 } 108 109 for i := 0; i < NSequential; i++ { 110 add(testKey(i), hex(uint64(i))) 111 } 112 for i := 0; i < NCollision; i++ { 113 add(testKey(uint64(i+1)<<32), "collision "+hex(uint64(i))) 114 } 115 for i := 0; i < NRandom; i++ { 116 // Avoid rand.Uint64 for compatibility with pre 1.8 Go 117 k := uint64(rand.Int63())>>31 | uint64(rand.Int63())<<32 118 add(testKey(k), "random "+hex(k)) 119 } 120 for i := 0; i < NReplace; i++ { 121 k := uint64(rand.Int31n(NSequential)) 122 add(testKey(k), "replace "+hex(k)) 123 } 124 125 testHashMapWithRefEntries(t, refEntries) 126 } 127 128 func TestHashMapSmallRandom(t *testing.T) { 129 for p := 0; p < SmallRandomPass; p++ { 130 var refEntries []refEntry 131 add := func(k testKey, v string) { 132 refEntries = append(refEntries, refEntry{k, v}) 133 } 134 135 for i := 0; i < NSmallRandom; i++ { 136 k := uint64(uint64(rand.Int31n(SmallRandomHighBound))<<32 | 137 uint64(rand.Int31n(SmallRandomLowBound))) 138 add(testKey(k), "random "+hex(k)) 139 } 140 141 testHashMapWithRefEntries(t, refEntries) 142 } 143 } 144 145 var marshalJSONTests = []struct { 146 in Map 147 wantOut string 148 wantErr bool 149 }{ 150 {makeHashMap(uint32(1), "a", "2", "b"), `{"1":"a","2":"b"}`, false}, 151 // Invalid key type 152 {makeHashMap([]any{}, "x"), "", true}, 153 } 154 155 func TestMarshalJSON(t *testing.T) { 156 for i, test := range marshalJSONTests { 157 out, err := test.in.MarshalJSON() 158 if string(out) != test.wantOut { 159 t.Errorf("m%d.MarshalJSON -> out %s, want %s", i, out, test.wantOut) 160 } 161 if (err != nil) != test.wantErr { 162 var wantErr string 163 if test.wantErr { 164 wantErr = "non-nil" 165 } else { 166 wantErr = "nil" 167 } 168 t.Errorf("m%d.MarshalJSON -> err %v, want %s", i, err, wantErr) 169 } 170 } 171 } 172 173 func makeHashMap(data ...any) Map { 174 m := empty 175 for i := 0; i+1 < len(data); i += 2 { 176 k, v := data[i], data[i+1] 177 m = m.Assoc(k, v) 178 } 179 return m 180 } 181 182 // testHashMapWithRefEntries tests the operations of a Map. It uses the supplied 183 // list of entries to build the map, and then test all its operations. 184 func testHashMapWithRefEntries(t *testing.T, refEntries []refEntry) { 185 m := empty 186 // Len of Empty should be 0. 187 if m.Len() != 0 { 188 t.Errorf("m.Len = %d, want %d", m.Len(), 0) 189 } 190 191 // Assoc and Len, test by building a map simultaneously. 192 ref := make(map[testKey]string, len(refEntries)) 193 for _, e := range refEntries { 194 ref[e.k] = e.v 195 m = m.Assoc(e.k, e.v) 196 if m.Len() != len(ref) { 197 t.Errorf("m.Len = %d, want %d", m.Len(), len(ref)) 198 } 199 } 200 201 // Index. 202 testMapContent(t, m, ref) 203 got, in := m.Index(anotherTestKey(0)) 204 if in { 205 t.Errorf("m.Index <bad key> returns entry %v", got) 206 } 207 // Iterator. 208 testIterator(t, m, ref) 209 210 // Dissoc. 211 // Ineffective ones. 212 for i := 0; i < NIneffectiveDissoc; i++ { 213 k := anotherTestKey(uint32(rand.Int31())>>15 | uint32(rand.Int31())<<16) 214 m = m.Dissoc(k) 215 if m.Len() != len(ref) { 216 t.Errorf("m.Dissoc removes item when it shouldn't") 217 } 218 } 219 220 // Effective ones. 221 for x := 0; x < len(refEntries); x++ { 222 i := rand.Intn(len(refEntries)) 223 k := refEntries[i].k 224 delete(ref, k) 225 m = m.Dissoc(k) 226 if m.Len() != len(ref) { 227 t.Errorf("m.Len() = %d after removing, should be %v", m.Len(), len(ref)) 228 } 229 _, in := m.Index(k) 230 if in { 231 t.Errorf("m.Index(%v) still returns item after removal", k) 232 } 233 // Checking all elements is expensive. Only do this 1% of the time. 234 if rand.Float64() < 0.01 { 235 testMapContent(t, m, ref) 236 testIterator(t, m, ref) 237 } 238 } 239 } 240 241 func testMapContent(t *testing.T, m Map, ref map[testKey]string) { 242 for k, v := range ref { 243 got, in := m.Index(k) 244 if !in { 245 t.Errorf("m.Index 0x%x returns no entry", k) 246 } 247 if got != v { 248 t.Errorf("m.Index(0x%x) = %v, want %v", k, got, v) 249 } 250 } 251 } 252 253 func testIterator(t *testing.T, m Map, ref map[testKey]string) { 254 ref2 := map[any]any{} 255 for k, v := range ref { 256 ref2[k] = v 257 } 258 for it := m.Iterator(); it.HasElem(); it.Next() { 259 k, v := it.Elem() 260 if ref2[k] != v { 261 t.Errorf("iterator yields unexpected pair %v, %v", k, v) 262 } 263 delete(ref2, k) 264 } 265 if len(ref2) != 0 { 266 t.Errorf("iterating was not exhaustive") 267 } 268 } 269 270 func TestNilKey(t *testing.T) { 271 m := empty 272 273 testLen := func(l int) { 274 if m.Len() != l { 275 t.Errorf(".Len -> %d, want %d", m.Len(), l) 276 } 277 } 278 testIndex := func(wantVal any, wantOk bool) { 279 val, ok := m.Index(nil) 280 if val != wantVal { 281 t.Errorf(".Index -> %v, want %v", val, wantVal) 282 } 283 if ok != wantOk { 284 t.Errorf(".Index -> ok %v, want %v", ok, wantOk) 285 } 286 } 287 288 testLen(0) 289 testIndex(nil, false) 290 291 m = m.Assoc(nil, "nil value") 292 testLen(1) 293 testIndex("nil value", true) 294 295 m = m.Assoc(nil, "nil value 2") 296 testLen(1) 297 testIndex("nil value 2", true) 298 299 m = m.Dissoc(nil) 300 testLen(0) 301 testIndex(nil, false) 302 } 303 304 func TestIterateMapWithNilKey(t *testing.T) { 305 m := empty.Assoc("k", "v").Assoc(nil, "nil value") 306 var collected []any 307 for it := m.Iterator(); it.HasElem(); it.Next() { 308 k, v := it.Elem() 309 collected = append(collected, k, v) 310 } 311 wantCollected := []any{nil, "nil value", "k", "v"} 312 if !reflect.DeepEqual(collected, wantCollected) { 313 t.Errorf("collected %v, want %v", collected, wantCollected) 314 } 315 } 316 317 func BenchmarkSequentialConjNative1(b *testing.B) { nativeSequentialAdd(b.N, N1) } 318 func BenchmarkSequentialConjNative2(b *testing.B) { nativeSequentialAdd(b.N, N2) } 319 func BenchmarkSequentialConjNative3(b *testing.B) { nativeSequentialAdd(b.N, N3) } 320 321 // nativeSequentialAdd starts with an empty native map and adds elements 0...n-1 322 // to the map, using the same value as the key, repeating for N times. 323 func nativeSequentialAdd(N int, n uint32) { 324 for r := 0; r < N; r++ { 325 m := make(map[uint32]uint32) 326 for i := uint32(0); i < n; i++ { 327 m[i] = i 328 } 329 } 330 } 331 332 func BenchmarkSequentialConjPersistent1(b *testing.B) { sequentialConj(b.N, N1) } 333 func BenchmarkSequentialConjPersistent2(b *testing.B) { sequentialConj(b.N, N2) } 334 func BenchmarkSequentialConjPersistent3(b *testing.B) { sequentialConj(b.N, N3) } 335 336 // sequentialConj starts with an empty hash map and adds elements 0...n-1 to the 337 // map, using the same value as the key, repeating for N times. 338 func sequentialConj(N int, n uint32) { 339 for r := 0; r < N; r++ { 340 m := empty 341 for i := uint32(0); i < n; i++ { 342 m = m.Assoc(i, i) 343 } 344 } 345 } 346 347 func BenchmarkRandomStringsConjNative1(b *testing.B) { nativeRandomStringsAdd(b, N1) } 348 func BenchmarkRandomStringsConjNative2(b *testing.B) { nativeRandomStringsAdd(b, N2) } 349 func BenchmarkRandomStringsConjNative3(b *testing.B) { nativeRandomStringsAdd(b, N3) } 350 351 // nativeRandomStringsAdd starts with an empty native map and adds n random strings 352 // to the map, using the same value as the key, repeating for b.N times. 353 func nativeRandomStringsAdd(b *testing.B, n int) { 354 ss := getRandomStrings(b) 355 for r := 0; r < b.N; r++ { 356 m := make(map[string]string) 357 for i := 0; i < n; i++ { 358 s := ss[i] 359 m[s] = s 360 } 361 } 362 } 363 364 func BenchmarkRandomStringsConjPersistent1(b *testing.B) { randomStringsConj(b, N1) } 365 func BenchmarkRandomStringsConjPersistent2(b *testing.B) { randomStringsConj(b, N2) } 366 func BenchmarkRandomStringsConjPersistent3(b *testing.B) { randomStringsConj(b, N3) } 367 368 func randomStringsConj(b *testing.B, n int) { 369 ss := getRandomStrings(b) 370 for r := 0; r < b.N; r++ { 371 m := empty 372 for i := 0; i < n; i++ { 373 s := ss[i] 374 m = m.Assoc(s, s) 375 } 376 } 377 }