From d07f76207b54dd98505d06cb4e9f01a311399cd5 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 21 Mar 2026 13:07:11 -0400 Subject: [PATCH 1/4] feat: add options to fuzz testing - Added the options to `fuzzScenario`. - They are clamped to non-panic values, so it only tests viable combinations. --- cuckoo_fuzz_test.go | 24 ++++++++++++++++++++---- settings.go | 18 ++++++++++++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/cuckoo_fuzz_test.go b/cuckoo_fuzz_test.go index fd97b2f..cfc6b73 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) { @@ -44,10 +49,21 @@ func FuzzInsertLookup(f *testing.F) { return } + seedA, seedB := scenario.seedA, scenario.seedB + growthFactor := max(1, int(scenario.growthFactor)) + capacity := int(scenario.capacity) + minimumLoad := math.Abs(math.Mod(scenario.load, 1.0)) + + 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..b9f78cc 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,31 @@ 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. 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. +// it must resize. The value must be greater than 0. func GrowthFactor(value int) Option { + if value <= 0 { + panic(fmt.Sprintf("go-cuckoo: GrowthFactor must be greater than 0, got %d", value)) + } + return func(s *settings) { s.growthFactor = uint64(value) } } -- 2.49.1 From 9e936ebadb4ec08ce0d3c3b5340d50be50f6207d Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 21 Mar 2026 13:13:51 -0400 Subject: [PATCH 2/4] fix: prevent users from settings a growthfactor=1 - This always causes errors, it should not be possible. --- cuckoo_fuzz_test.go | 2 +- settings.go | 6 +++--- testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec diff --git a/cuckoo_fuzz_test.go b/cuckoo_fuzz_test.go index cfc6b73..db61710 100644 --- a/cuckoo_fuzz_test.go +++ b/cuckoo_fuzz_test.go @@ -50,7 +50,7 @@ func FuzzInsertLookup(f *testing.F) { } seedA, seedB := scenario.seedA, scenario.seedB - growthFactor := max(1, int(scenario.growthFactor)) + growthFactor := max(2, int(scenario.growthFactor)) capacity := int(scenario.capacity) minimumLoad := math.Abs(math.Mod(scenario.load, 1.0)) diff --git a/settings.go b/settings.go index b9f78cc..6126f64 100644 --- a/settings.go +++ b/settings.go @@ -49,10 +49,10 @@ func MinimumLoad(value float64) Option { } // GrowthFactor controls how much the capacity of the [Table] multiplies when -// it must resize. The value must be greater than 0. +// it must resize. The value must be greater than 1. func GrowthFactor(value int) Option { - if value <= 0 { - panic(fmt.Sprintf("go-cuckoo: GrowthFactor must be greater than 0, got %d", value)) + 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) } diff --git a/testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec b/testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec new file mode 100644 index 0000000..9e228ad --- /dev/null +++ b/testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("1000010X17000A11\xf5XA000\xe00001aA120000000000000111220000122910000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") -- 2.49.1 From b499fa9c053eb6f621341ffb715292ad431743d4 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Tue, 24 Mar 2026 20:26:20 -0400 Subject: [PATCH 3/4] fix: reduce minimum load in tests to <20% --- cuckoo_fuzz_test.go | 16 ++++++++++++---- testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec | 2 -- 2 files changed, 12 insertions(+), 6 deletions(-) delete mode 100644 testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec diff --git a/cuckoo_fuzz_test.go b/cuckoo_fuzz_test.go index db61710..80deea7 100644 --- a/cuckoo_fuzz_test.go +++ b/cuckoo_fuzz_test.go @@ -45,15 +45,23 @@ 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) diff --git a/testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec b/testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec deleted file mode 100644 index 9e228ad..0000000 --- a/testdata/fuzz/FuzzInsertLookup/a79aad05d13023ec +++ /dev/null @@ -1,2 +0,0 @@ -go test fuzz v1 -[]byte("1000010X17000A11\xf5XA000\xe00001aA120000000000000111220000122910000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") -- 2.49.1 From a815b49b66452935d8808e26dd4e8b36ce18a54d Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Tue, 24 Mar 2026 20:59:04 -0400 Subject: [PATCH 4/4] docs: information about recommended minimum load --- settings.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/settings.go b/settings.go index 6126f64..3301b4f 100644 --- a/settings.go +++ b/settings.go @@ -40,6 +40,9 @@ func Capacity(value int) Option { // 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)) -- 2.49.1