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  }