mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-14 19:27:58 +08:00
383 lines
9.2 KiB
Go
383 lines
9.2 KiB
Go
package hashmap
|
|
|
|
import (
|
|
"math/rand"
|
|
"reflect"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"src.elv.sh/pkg/persistent/hash"
|
|
)
|
|
|
|
const (
|
|
NSequential = 0x1000
|
|
NCollision = 0x100
|
|
NRandom = 0x4000
|
|
NReplace = 0x200
|
|
|
|
SmallRandomPass = 0x100
|
|
NSmallRandom = 0x400
|
|
SmallRandomHighBound = 0x50
|
|
SmallRandomLowBound = 0x200
|
|
|
|
NArrayNode = 0x100
|
|
|
|
NIneffectiveDissoc = 0x200
|
|
|
|
N1 = nodeCap + 1
|
|
N2 = nodeCap*nodeCap + 1
|
|
N3 = nodeCap*nodeCap*nodeCap + 1
|
|
)
|
|
|
|
type testKey uint64
|
|
type anotherTestKey uint32
|
|
|
|
func equalFunc(k1, k2 any) bool {
|
|
switch k1 := k1.(type) {
|
|
case testKey:
|
|
t2, ok := k2.(testKey)
|
|
return ok && k1 == t2
|
|
case anotherTestKey:
|
|
return false
|
|
default:
|
|
return k1 == k2
|
|
}
|
|
}
|
|
|
|
func hashFunc(k any) uint32 {
|
|
switch k := k.(type) {
|
|
case uint32:
|
|
return k
|
|
case string:
|
|
return hash.String(k)
|
|
case testKey:
|
|
// Return the lower 32 bits for testKey. This is intended so that hash
|
|
// collisions can be easily constructed.
|
|
return uint32(k & 0xffffffff)
|
|
case anotherTestKey:
|
|
return uint32(k)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
var empty = New(equalFunc, hashFunc)
|
|
|
|
type refEntry struct {
|
|
k testKey
|
|
v string
|
|
}
|
|
|
|
func hex(i uint64) string {
|
|
return "0x" + strconv.FormatUint(i, 16)
|
|
}
|
|
|
|
func init() {
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
}
|
|
|
|
var randomStrings []string
|
|
|
|
// getRandomStrings returns a slice of N3 random strings. It builds the slice
|
|
// once and caches it. If the slice is built for the first time, it stops the
|
|
// timer of the benchmark.
|
|
func getRandomStrings(b *testing.B) []string {
|
|
if randomStrings == nil {
|
|
b.StopTimer()
|
|
defer b.StartTimer()
|
|
randomStrings = make([]string, N3)
|
|
for i := 0; i < N3; i++ {
|
|
randomStrings[i] = makeRandomString()
|
|
}
|
|
}
|
|
return randomStrings
|
|
}
|
|
|
|
// makeRandomString builds a random string consisting of n bytes (randomized
|
|
// between 0 and 99) and each byte is randomized between 0 and 255. The string
|
|
// need not be valid UTF-8.
|
|
func makeRandomString() string {
|
|
bytes := make([]byte, rand.Intn(100))
|
|
for i := range bytes {
|
|
bytes[i] = byte(rand.Intn(256))
|
|
}
|
|
return string(bytes)
|
|
}
|
|
|
|
func TestHashMap(t *testing.T) {
|
|
var refEntries []refEntry
|
|
add := func(k testKey, v string) {
|
|
refEntries = append(refEntries, refEntry{k, v})
|
|
}
|
|
|
|
for i := 0; i < NSequential; i++ {
|
|
add(testKey(i), hex(uint64(i)))
|
|
}
|
|
for i := 0; i < NCollision; i++ {
|
|
add(testKey(uint64(i+1)<<32), "collision "+hex(uint64(i)))
|
|
}
|
|
for i := 0; i < NRandom; i++ {
|
|
// Avoid rand.Uint64 for compatibility with pre 1.8 Go
|
|
k := uint64(rand.Int63())>>31 | uint64(rand.Int63())<<32
|
|
add(testKey(k), "random "+hex(k))
|
|
}
|
|
for i := 0; i < NReplace; i++ {
|
|
k := uint64(rand.Int31n(NSequential))
|
|
add(testKey(k), "replace "+hex(k))
|
|
}
|
|
|
|
testHashMapWithRefEntries(t, refEntries)
|
|
}
|
|
|
|
func TestHashMapSmallRandom(t *testing.T) {
|
|
for p := 0; p < SmallRandomPass; p++ {
|
|
var refEntries []refEntry
|
|
add := func(k testKey, v string) {
|
|
refEntries = append(refEntries, refEntry{k, v})
|
|
}
|
|
|
|
for i := 0; i < NSmallRandom; i++ {
|
|
k := uint64(uint64(rand.Int31n(SmallRandomHighBound))<<32 |
|
|
uint64(rand.Int31n(SmallRandomLowBound)))
|
|
add(testKey(k), "random "+hex(k))
|
|
}
|
|
|
|
testHashMapWithRefEntries(t, refEntries)
|
|
}
|
|
}
|
|
|
|
var marshalJSONTests = []struct {
|
|
in Map
|
|
wantOut string
|
|
wantErr bool
|
|
}{
|
|
{makeHashMap(uint32(1), "a", "2", "b"), `{"1":"a","2":"b"}`, false},
|
|
// Invalid key type
|
|
{makeHashMap([]any{}, "x"), "", true},
|
|
}
|
|
|
|
func TestMarshalJSON(t *testing.T) {
|
|
for i, test := range marshalJSONTests {
|
|
out, err := test.in.MarshalJSON()
|
|
if string(out) != test.wantOut {
|
|
t.Errorf("m%d.MarshalJSON -> out %s, want %s", i, out, test.wantOut)
|
|
}
|
|
if (err != nil) != test.wantErr {
|
|
var wantErr string
|
|
if test.wantErr {
|
|
wantErr = "non-nil"
|
|
} else {
|
|
wantErr = "nil"
|
|
}
|
|
t.Errorf("m%d.MarshalJSON -> err %v, want %s", i, err, wantErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeHashMap(data ...any) Map {
|
|
m := empty
|
|
for i := 0; i+1 < len(data); i += 2 {
|
|
k, v := data[i], data[i+1]
|
|
m = m.Assoc(k, v)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// testHashMapWithRefEntries tests the operations of a Map. It uses the supplied
|
|
// list of entries to build the map, and then test all its operations.
|
|
func testHashMapWithRefEntries(t *testing.T, refEntries []refEntry) {
|
|
m := empty
|
|
// Len of Empty should be 0.
|
|
if m.Len() != 0 {
|
|
t.Errorf("m.Len = %d, want %d", m.Len(), 0)
|
|
}
|
|
|
|
// Assoc and Len, test by building a map simultaneously.
|
|
ref := make(map[testKey]string, len(refEntries))
|
|
for _, e := range refEntries {
|
|
ref[e.k] = e.v
|
|
m = m.Assoc(e.k, e.v)
|
|
if m.Len() != len(ref) {
|
|
t.Errorf("m.Len = %d, want %d", m.Len(), len(ref))
|
|
}
|
|
}
|
|
|
|
// Index.
|
|
testMapContent(t, m, ref)
|
|
got, in := m.Index(anotherTestKey(0))
|
|
if in {
|
|
t.Errorf("m.Index <bad key> returns entry %v", got)
|
|
}
|
|
// Iterator.
|
|
testIterator(t, m, ref)
|
|
|
|
// Dissoc.
|
|
// Ineffective ones.
|
|
for i := 0; i < NIneffectiveDissoc; i++ {
|
|
k := anotherTestKey(uint32(rand.Int31())>>15 | uint32(rand.Int31())<<16)
|
|
m = m.Dissoc(k)
|
|
if m.Len() != len(ref) {
|
|
t.Errorf("m.Dissoc removes item when it shouldn't")
|
|
}
|
|
}
|
|
|
|
// Effective ones.
|
|
for x := 0; x < len(refEntries); x++ {
|
|
i := rand.Intn(len(refEntries))
|
|
k := refEntries[i].k
|
|
delete(ref, k)
|
|
m = m.Dissoc(k)
|
|
if m.Len() != len(ref) {
|
|
t.Errorf("m.Len() = %d after removing, should be %v", m.Len(), len(ref))
|
|
}
|
|
_, in := m.Index(k)
|
|
if in {
|
|
t.Errorf("m.Index(%v) still returns item after removal", k)
|
|
}
|
|
// Checking all elements is expensive. Only do this 1% of the time.
|
|
if rand.Float64() < 0.01 {
|
|
testMapContent(t, m, ref)
|
|
testIterator(t, m, ref)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testMapContent(t *testing.T, m Map, ref map[testKey]string) {
|
|
for k, v := range ref {
|
|
got, in := m.Index(k)
|
|
if !in {
|
|
t.Errorf("m.Index 0x%x returns no entry", k)
|
|
}
|
|
if got != v {
|
|
t.Errorf("m.Index(0x%x) = %v, want %v", k, got, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testIterator(t *testing.T, m Map, ref map[testKey]string) {
|
|
ref2 := map[any]any{}
|
|
for k, v := range ref {
|
|
ref2[k] = v
|
|
}
|
|
for it := m.Iterator(); it.HasElem(); it.Next() {
|
|
k, v := it.Elem()
|
|
if ref2[k] != v {
|
|
t.Errorf("iterator yields unexpected pair %v, %v", k, v)
|
|
}
|
|
delete(ref2, k)
|
|
}
|
|
if len(ref2) != 0 {
|
|
t.Errorf("iterating was not exhaustive")
|
|
}
|
|
}
|
|
|
|
func TestNilKey(t *testing.T) {
|
|
m := empty
|
|
|
|
testLen := func(l int) {
|
|
if m.Len() != l {
|
|
t.Errorf(".Len -> %d, want %d", m.Len(), l)
|
|
}
|
|
}
|
|
testIndex := func(wantVal any, wantOk bool) {
|
|
val, ok := m.Index(nil)
|
|
if val != wantVal {
|
|
t.Errorf(".Index -> %v, want %v", val, wantVal)
|
|
}
|
|
if ok != wantOk {
|
|
t.Errorf(".Index -> ok %v, want %v", ok, wantOk)
|
|
}
|
|
}
|
|
|
|
testLen(0)
|
|
testIndex(nil, false)
|
|
|
|
m = m.Assoc(nil, "nil value")
|
|
testLen(1)
|
|
testIndex("nil value", true)
|
|
|
|
m = m.Assoc(nil, "nil value 2")
|
|
testLen(1)
|
|
testIndex("nil value 2", true)
|
|
|
|
m = m.Dissoc(nil)
|
|
testLen(0)
|
|
testIndex(nil, false)
|
|
}
|
|
|
|
func TestIterateMapWithNilKey(t *testing.T) {
|
|
m := empty.Assoc("k", "v").Assoc(nil, "nil value")
|
|
var collected []any
|
|
for it := m.Iterator(); it.HasElem(); it.Next() {
|
|
k, v := it.Elem()
|
|
collected = append(collected, k, v)
|
|
}
|
|
wantCollected := []any{nil, "nil value", "k", "v"}
|
|
if !reflect.DeepEqual(collected, wantCollected) {
|
|
t.Errorf("collected %v, want %v", collected, wantCollected)
|
|
}
|
|
}
|
|
|
|
func BenchmarkSequentialConjNative1(b *testing.B) { nativeSequentialAdd(b.N, N1) }
|
|
func BenchmarkSequentialConjNative2(b *testing.B) { nativeSequentialAdd(b.N, N2) }
|
|
func BenchmarkSequentialConjNative3(b *testing.B) { nativeSequentialAdd(b.N, N3) }
|
|
|
|
// nativeSequntialAdd starts with an empty native map and adds elements 0...n-1
|
|
// to the map, using the same value as the key, repeating for N times.
|
|
func nativeSequentialAdd(N int, n uint32) {
|
|
for r := 0; r < N; r++ {
|
|
m := make(map[uint32]uint32)
|
|
for i := uint32(0); i < n; i++ {
|
|
m[i] = i
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSequentialConjPersistent1(b *testing.B) { sequentialConj(b.N, N1) }
|
|
func BenchmarkSequentialConjPersistent2(b *testing.B) { sequentialConj(b.N, N2) }
|
|
func BenchmarkSequentialConjPersistent3(b *testing.B) { sequentialConj(b.N, N3) }
|
|
|
|
// sequentialConj starts with an empty hash map and adds elements 0...n-1 to the
|
|
// map, using the same value as the key, repeating for N times.
|
|
func sequentialConj(N int, n uint32) {
|
|
for r := 0; r < N; r++ {
|
|
m := empty
|
|
for i := uint32(0); i < n; i++ {
|
|
m = m.Assoc(i, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkRandomStringsConjNative1(b *testing.B) { nativeRandomStringsAdd(b, N1) }
|
|
func BenchmarkRandomStringsConjNative2(b *testing.B) { nativeRandomStringsAdd(b, N2) }
|
|
func BenchmarkRandomStringsConjNative3(b *testing.B) { nativeRandomStringsAdd(b, N3) }
|
|
|
|
// nativeSequntialAdd starts with an empty native map and adds n random strings
|
|
// to the map, using the same value as the key, repeating for b.N times.
|
|
func nativeRandomStringsAdd(b *testing.B, n int) {
|
|
ss := getRandomStrings(b)
|
|
for r := 0; r < b.N; r++ {
|
|
m := make(map[string]string)
|
|
for i := 0; i < n; i++ {
|
|
s := ss[i]
|
|
m[s] = s
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkRandomStringsConjPersistent1(b *testing.B) { randomStringsConj(b, N1) }
|
|
func BenchmarkRandomStringsConjPersistent2(b *testing.B) { randomStringsConj(b, N2) }
|
|
func BenchmarkRandomStringsConjPersistent3(b *testing.B) { randomStringsConj(b, N3) }
|
|
|
|
func randomStringsConj(b *testing.B, n int) {
|
|
ss := getRandomStrings(b)
|
|
for r := 0; r < b.N; r++ {
|
|
m := empty
|
|
for i := 0; i < n; i++ {
|
|
s := ss[i]
|
|
m = m.Assoc(s, s)
|
|
}
|
|
}
|
|
}
|