All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Go Lint (push) Successful in 43s
CI / Makefile Lint (push) Successful in 41s
CI / Markdown Lint (push) Successful in 32s
CI / Unit Tests (push) Successful in 39s
CI / Fuzz Tests (push) Successful in 1m44s
CI / Mutation Tests (push) Successful in 1m28s
## Description
Currently, the signature for `Table.Get` is `func (K) (V, error)`. This is not very Go-idiomatic, which prefers to return a boolean instead of an error. For instance, a built-in Go map is used like so:
```go
if value, ok := users[id]; !ok {
// ...
}
```
Updating our table to look like that is best practice. In that same vein, to support direct lookup (i.e. `v := users[id]`), this PR also adds `Table.Find`.
## Changes
- BREAKING CHANGE: Update contract of `Table.Get` to `func (K) (V, bool)`. Returns 'false' is the item cannot be found, and 'true' if it is found.
- Add `Table.Find`.
- Updated tests and documentation to match the change.
### Design Decisions
- Chose to make this decision because throwing an error implies that there is something 'wrong' with the table. There is nothing wrong with the table; it is just that the item does not exist.
## Checklist
- [x] Tests pass
- [x] Docs updated
Reviewed-on: #20
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
230 lines
4.6 KiB
Go
230 lines
4.6 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.NewTable[int, bool]()
|
|
|
|
assert.NotNil(table)
|
|
assert.Zero(table.Size())
|
|
}
|
|
|
|
func TestAddItem(t *testing.T) {
|
|
assert := assert.New(t)
|
|
key, value := 0, true
|
|
table := cuckoo.NewTable[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.NewTable[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.NewCustomTable[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.NewTable[int, bool](cuckoo.Capacity(64))
|
|
|
|
assert.Equal(uint64(128), table.TotalCapacity())
|
|
}
|
|
|
|
func TestResizeCapacity(t *testing.T) {
|
|
assert := assert.New(t)
|
|
table := cuckoo.NewTable[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.NewTable[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.NewTable[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.NewTable[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.NewTable[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.NewTable[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.NewTable[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.NewCustomTable[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.NewTable[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.NewTableBy[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"}))
|
|
}
|