diff --git a/cuckoo_fuzz_test.go b/cuckoo_fuzz_test.go index fd97b2f..80deea7 100644 --- a/cuckoo_fuzz_test.go +++ b/cuckoo_fuzz_test.go @@ -1,7 +1,10 @@ package cuckoo_test import ( + "fmt" "maps" + "math" + "os" "testing" "github.com/stretchr/testify/assert" @@ -25,8 +28,10 @@ type fuzzStep struct { } type fuzzScenario struct { - seedA, seedB uint32 - steps []fuzzStep + seedA, seedB uint32 + capacity, growthFactor uint8 + load float64 + steps []fuzzStep } func FuzzInsertLookup(f *testing.F) { @@ -40,14 +45,33 @@ func FuzzInsertLookup(f *testing.F) { return } - if scenario.seedA == scenario.seedB { - return + seedA, seedB := scenario.seedA, scenario.seedB + growthFactor := max(2, int(scenario.growthFactor)) + capacity := int(scenario.capacity) + minimumLoad := math.Abs(math.Mod(scenario.load, 1.0)) + + // If they are the same number, the hashes will clash, always causing an + // error. + if seedA == seedB { + t.Skip() } + // If the load is too high, the hashs will not be able to allocate + // properly. + if minimumLoad > 0.20 { + t.Skip() + } + + fmt.Fprintf(os.Stderr, "seedA=%d seedB=%d capacity=%d growthFactor=%d minimumLoad=%f\n", + seedA, seedB, capacity, growthFactor, minimumLoad) + actual := cuckoo.NewCustomTable[uint32, uint32]( - offsetHash(scenario.seedA), - offsetHash(scenario.seedB), + offsetHash(seedA), + offsetHash(seedB), func(a, b uint32) bool { return a == b }, + cuckoo.Capacity(capacity), + cuckoo.GrowthFactor(growthFactor), + cuckoo.MinimumLoad(minimumLoad), ) expected := map[uint32]uint32{} diff --git a/settings.go b/settings.go index c96ee45..3301b4f 100644 --- a/settings.go +++ b/settings.go @@ -1,5 +1,7 @@ package cuckoo +import "fmt" + // DefaultCapacity is the initial capacity of a [Table]. It is inspired from // Java's [HashMap] implementation, which also uses 16. // @@ -27,19 +29,34 @@ type settings struct { type Option func(*settings) // Capacity modifies the starting capacity of each bucket of the [Table]. The -// value must be greater than 0. +// value must be non-negative. func Capacity(value int) Option { + if value < 0 { + panic(fmt.Sprintf("go-cuckoo: Capacity must be non-negative, got %d", value)) + } + return func(s *settings) { s.bucketSize = uint64(value) } } // MinimumLoad modifies the [DefaultMinimumLoad] of the [Table]. The value must // be between 0.00 and 1.00. +// +// The higher the minimum load, the more likely that a [Table.Put] will not +// succeed. Minimum loads above 20% are not tested. func MinimumLoad(value float64) Option { + if value < 0.00 || value > 1.00 { + panic(fmt.Sprintf("go-cuckoo: MinimumLoad must be between 0.00 and 1.00, got %f", value)) + } + return func(s *settings) { s.minLoadFactor = value } } // GrowthFactor controls how much the capacity of the [Table] multiplies when // it must resize. The value must be greater than 1. func GrowthFactor(value int) Option { + if value < 2 { + panic(fmt.Sprintf("go-cuckoo: GrowthFactor must be greater than 1, got %d", value)) + } + return func(s *settings) { s.growthFactor = uint64(value) } }