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>
93 lines
2.0 KiB
Go
93 lines
2.0 KiB
Go
package cuckoo_test
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
go_fuzz_utils "github.com/trailofbits/go-fuzz-utils"
|
|
|
|
"git.maximhutz.com/tools/go-cuckoo"
|
|
)
|
|
|
|
func offsetHash(seed uint32) cuckoo.Hash[uint32] {
|
|
return func(x uint32) uint64 {
|
|
v := uint64(x) ^ uint64(seed)
|
|
v = (v ^ (v >> 30)) * 0xbf58476d1ce4e5b9
|
|
v = (v ^ (v >> 27)) * 0x94d049bb133111eb
|
|
return v ^ (v >> 31)
|
|
}
|
|
}
|
|
|
|
type fuzzStep struct {
|
|
drop bool
|
|
key, value uint32
|
|
}
|
|
|
|
type fuzzScenario struct {
|
|
seedA, seedB uint32
|
|
capacity, growthFactor uint8
|
|
steps []fuzzStep
|
|
}
|
|
|
|
func FuzzInsertLookup(f *testing.F) {
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
var scenario fuzzScenario
|
|
assert := assert.New(t)
|
|
|
|
if tp, err := go_fuzz_utils.NewTypeProvider(data); err != nil {
|
|
return
|
|
} else if err := tp.Fill(&scenario); err != nil {
|
|
return
|
|
}
|
|
|
|
seedA, seedB := scenario.seedA, scenario.seedB
|
|
growthFactor := max(2, int(scenario.growthFactor))
|
|
capacity := int(scenario.capacity)
|
|
|
|
// If they are the same number, the hashes will clash, always causing an
|
|
// error.
|
|
if seedA == seedB {
|
|
t.Skip()
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "seedA=%d seedB=%d capacity=%d growthFactor=%d\n",
|
|
seedA, seedB, capacity, growthFactor)
|
|
|
|
actual := cuckoo.NewCustomTable[uint32, uint32](
|
|
offsetHash(seedA),
|
|
offsetHash(seedB),
|
|
func(a, b uint32) bool { return a == b },
|
|
cuckoo.Capacity(capacity),
|
|
cuckoo.GrowthFactor(growthFactor),
|
|
)
|
|
|
|
expected := map[uint32]uint32{}
|
|
|
|
for _, step := range scenario.steps {
|
|
if step.drop {
|
|
err := actual.Drop(step.key)
|
|
assert.NoError(err)
|
|
|
|
delete(expected, step.key)
|
|
|
|
_, ok := actual.Get(step.key)
|
|
assert.False(ok)
|
|
} else {
|
|
err := actual.Put(step.key, step.value)
|
|
assert.NoError(err)
|
|
|
|
expected[step.key] = step.value
|
|
|
|
found, ok := actual.Get(step.key)
|
|
assert.True(ok)
|
|
assert.Equal(step.value, found)
|
|
}
|
|
|
|
assert.Equal(expected, maps.Collect(actual.Entries()))
|
|
}
|
|
})
|
|
}
|