Files
go-cuckoo/cuckoo_test.go
M.V. Hutz 7cc1657403
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Makefile Lint (push) Successful in 47s
CI / Go Lint (push) Successful in 51s
CI / Markdown Lint (push) Successful in 46s
CI / Unit Tests (push) Successful in 47s
CI / Fuzz Tests (push) Successful in 1m19s
CI / Mutation Tests (push) Successful in 1m36s
refactor!: shorter constructors, bucketsubtable (#22)
## Description

Currently, the name of `bucket` is a bit confusing, because it is considered a 'table' in literature (as well as the whole hash table). A `bucket` is better described as a 'subtable', which is used by the total hash table to perform cuckoo hashing.

In addition, the constructors `NewTable`, `NewTableBy`, and `NewCustomTable` were given shorter names, because the package name `cuckoo` already implies that `New*` would create a hash table with cuckoo hashing. This package has one use-case, and so it unambiguous what constructors produce.

## Changes

- `NewTable` -> `New`
- `NewTableBy` -> `NewBy`
- `NewCustomTable` -> `NewCustom`
- `bucket` -> `subtable`

### Design Decisions

- I would have renamed `Table` and `subtable` to map equivalents, but 'submap' implies that a certain subsection of the map is contained within it, which isn't quite right.
- I chose not to go with `Map` and `table`, because of the split naming convention.

## Checklist

- [x] Tests pass
- [x] Docs updated

Reviewed-on: #22
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-16 03:15:39 +00:00

228 lines
4.5 KiB
Go

package cuckoo_test
import (
"errors"
"maps"
"math/rand/v2"
"testing"
"github.com/stretchr/testify/assert"
"git.maximhutz.com/tools/go-cuckoo"
)
func TestNewTable(t *testing.T) {
assert := assert.New(t)
table := cuckoo.New[int, bool]()
assert.NotNil(table)
assert.Zero(table.Size())
}
func TestAddItem(t *testing.T) {
assert := assert.New(t)
key, value := 0, true
table := cuckoo.New[int, bool]()
err := table.Put(key, value)
assert.NoError(err)
assert.Equal(1, table.Size())
assert.True(table.Has(key))
}
func TestPutOverwrite(t *testing.T) {
assert := assert.New(t)
key, value, newValue := 0, 1, 2
table := cuckoo.New[int, int]()
(table.Put(key, value))
err := table.Put(key, newValue)
assert.NoError(err)
assert.Equal(1, table.Size())
assert.True(table.Has(key))
found, _ := table.Get(key)
assert.Equal(newValue, found)
}
func TestSameHash(t *testing.T) {
assert := assert.New(t)
hash := func(int) uint64 { return 0 }
table := cuckoo.NewCustom[int, bool](hash, hash, cuckoo.DefaultEqualFunc[int])
errA := table.Put(0, true)
errB := table.Put(1, true)
errC := table.Put(2, true)
assert.NoError(errA)
assert.NoError(errB)
assert.ErrorContains(errC, "bad hash")
}
func TestStartingCapacity(t *testing.T) {
assert := assert.New(t)
table := cuckoo.New[int, bool](cuckoo.Capacity(64))
assert.Equal(uint64(128), table.TotalCapacity())
}
func TestResizeCapacity(t *testing.T) {
assert := assert.New(t)
table := cuckoo.New[int, bool](
cuckoo.Capacity(8),
cuckoo.GrowthFactor(2),
)
for table.TotalCapacity() == 16 {
err := table.Put(rand.Int(), true)
assert.NoError(err)
}
assert.Equal(uint64(32), table.TotalCapacity())
}
func TestPutMany(t *testing.T) {
assert := assert.New(t)
expected, actual := map[int]bool{}, cuckoo.New[int, bool]()
for i := range 1_000 {
expected[i] = true
err := actual.Put(i, true)
assert.NoError(err)
}
assert.Equal(maps.Collect(actual.Entries()), expected)
assert.Equal(len(expected), actual.Size())
}
func TestGetMany(t *testing.T) {
assert := assert.New(t)
table := cuckoo.New[int, bool]()
for i := range 1_000 {
err := table.Put(i, true)
assert.NoError(err)
}
for i := range 2_000 {
value, ok := table.Get(i)
if i < 1_000 {
assert.True(ok)
assert.Equal(value, true)
} else {
assert.False(ok)
}
}
}
func TestDropExistingItem(t *testing.T) {
assert := assert.New(t)
key, value := 0, true
table := cuckoo.New[int, bool]()
(table.Put(key, value))
err := table.Drop(key)
assert.NoError(err)
assert.Equal(0, table.Size())
assert.False(table.Has(key))
}
func TestDropNoItem(t *testing.T) {
assert := assert.New(t)
key := 0
table := cuckoo.New[int, bool]()
err := table.Drop(key)
assert.NoError(err)
assert.Equal(0, table.Size())
assert.False(table.Has(key))
}
func TestDropItemCapacity(t *testing.T) {
assert := assert.New(t)
key := 0
table := cuckoo.New[int, bool](
cuckoo.Capacity(64),
cuckoo.GrowthFactor(2),
)
startingCapacity := table.TotalCapacity()
err := table.Drop(key)
endingCapacity := table.TotalCapacity()
assert.NoError(err)
assert.Equal(0, table.Size())
assert.Equal(uint64(128), startingCapacity)
assert.Equal(uint64(64), endingCapacity)
}
func TestPutNoCapacity(t *testing.T) {
assert := assert.New(t)
key, value := 0, true
table := cuckoo.New[int, bool](
cuckoo.Capacity(0),
)
err := table.Put(key, value)
assert.NoError(err)
assert.Equal(1, table.Size())
assert.True(table.Has(key))
}
func TestBadHashCapacity(t *testing.T) {
assert := assert.New(t)
table := cuckoo.NewCustom[int, bool](
func(int) uint64 { return 0 },
func(int) uint64 { return 0 },
func(a, b int) bool { return a == b },
cuckoo.Capacity(20),
)
err1 := table.Put(0, true)
err2 := table.Put(1, true)
err3 := table.Put(2, true)
assert.NoError(err1)
assert.NoError(err2)
assert.Error(err3)
assert.Equal(uint64(80), table.TotalCapacity())
}
func TestDropResizeCapacity(t *testing.T) {
assert := assert.New(t)
table := cuckoo.New[int, bool](
cuckoo.Capacity(10),
)
err1 := table.Put(0, true)
err2 := table.Put(1, true)
err3 := table.Drop(1)
assert.NoError(errors.Join(err1, err2, err3))
assert.Equal(uint64(20), table.TotalCapacity())
}
func TestNewTableBy(t *testing.T) {
type User struct {
_ func()
id string
name string
}
assert := assert.New(t)
table := cuckoo.NewBy[User, bool](func(u User) string { return u.id })
err := table.Put(User{nil, "1", "Robert"}, true)
assert.NoError(err)
assert.Equal(1, table.Size())
assert.True(table.Has(User{nil, "1", "Robbie"}))
}