7 Commits

Author SHA1 Message Date
95687acade feat!: update contract for 'Table.Get()'
All checks were successful
CI / Check PR Title (pull_request) Successful in 19s
CI / Go Lint (pull_request) Successful in 42s
CI / Markdown Lint (pull_request) Successful in 22s
CI / Makefile Lint (pull_request) Successful in 40s
CI / Unit Tests (pull_request) Successful in 37s
CI / Fuzz Tests (pull_request) Successful in 1m12s
CI / Mutation Tests (pull_request) Successful in 1m18s
2026-04-03 21:27:48 +02:00
322d71f0be refactor!: remove MinimumLoad() option (#17)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Go Lint (push) Successful in 38s
CI / Makefile Lint (push) Successful in 36s
CI / Markdown Lint (push) Successful in 21s
CI / Unit Tests (push) Successful in 37s
CI / Fuzz Tests (push) Successful in 1m9s
CI / Mutation Tests (push) Successful in 1m18s
## Description

The `cuckoo.MinimumLoad()` option was not a very useful option, and prone to error. By removing the ability to modify it, and setting it to something reasonable (like 5%), we can remove a whole set of errors that the user may stumble into.

## Changes

- Remove `MinimumLoad()` option.
- Privated `DefaultMinimumLoad`.

### Design Decisions

- `DefaultMinimumLoad` should be privated because it is no longer an option. The user should not need to interact with it.

## Checklist

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

Reviewed-on: #17
2026-04-03 14:51:41 +00:00
ed30a4fc7c fix: check-pr-title job has prompt injection (#18)
All checks were successful
CI / Check PR Title (push) Has been skipped
CI / Go Lint (push) Successful in 36s
CI / Makefile Lint (push) Successful in 35s
CI / Markdown Lint (push) Successful in 22s
CI / Unit Tests (push) Successful in 35s
CI / Fuzz Tests (push) Successful in 1m6s
CI / Mutation Tests (push) Successful in 1m10s
## Description

Currently, the `check-pr-title` job has a security vulnerability. If you give the PR a bad title, the job can run arbitrary code.

## Changes

- Fix prompt injection by pulling the PR title as an environment variable.
- Also, restricted the job to only `pull_request` trigger.

### Design Decisions

- It is better to pull out this job into a separate workflow with a unique trigger, but I chose not to because it is currently only one job.

## Checklist

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

Reviewed-on: #18
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-03 14:47:01 +00:00
c834f004a2 chore: no free form issues, 'description'-s to 'placeholders'-s (#16)
Some checks failed
CI / Check PR Title (push) Failing after 19s
CI / Makefile Lint (push) Successful in 34s
CI / Go Lint (push) Successful in 37s
CI / Markdown Lint (push) Successful in 35s
CI / Unit Tests (push) Successful in 33s
CI / Fuzz Tests (push) Successful in 1m4s
CI / Mutation Tests (push) Successful in 2m55s
## Description

There were some problems with the roll-out of the issue templates. This PR addresses them.

## Changes

- Disable `blank-issue-enabled`.
- Move all `description` types in the issue templates to `placeholder`.

### Design Decisions

- The `description` fields take up too much space.

## Checklist

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

Reviewed-on: #16
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-03 13:57:03 +00:00
581f26f562 ci: support semantic versioning, conventional commits (#15)
Some checks failed
CI / Check PR Title (push) Failing after 18s
CI / Makefile Lint (push) Successful in 33s
CI / Markdown Lint (push) Successful in 22s
CI / Unit Tests (push) Successful in 30s
CI / Go Lint (push) Successful in 45s
CI / Mutation Tests (push) Successful in 1m18s
CI / Fuzz Tests (push) Successful in 1m48s
## Description

Currently, the repository is not well suited for semantic versioning, and hasn't much to support it. This PR adds templates, CI jobs, and configs to simplify its adoption.

## Changes

- Added `FEATURE` and `BUG` issue templates. Also, forbids free-form issues.
- Adds a PR template.
- Adds a CI job to ensure the commit title follows conventional commits.

### Design Decisions

N/A.

## Checklist

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

Reviewed-on: #15
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-04-03 13:46:05 +00:00
b1ab3a6326 chore: remove ./.claude/settings.json (#14)
All checks were successful
CI / Go Lint (push) Successful in 29s
CI / Makefile Lint (push) Successful in 28s
CI / Markdown Lint (push) Successful in 16s
CI / Unit Tests (push) Successful in 28s
CI / Fuzz Tests (push) Successful in 59s
CI / Mutation Tests (push) Successful in 1m7s
There shouldn't be a global settings folder for Claude in the repository. Also, add final newline to `.gitignore`.

Reviewed-on: #14
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-03-28 19:52:06 +00:00
36d76d4929 feat: wider fuzz tests, stronger options. (#9)
All checks were successful
CI / Go Lint (push) Successful in 38s
CI / Markdown Lint (push) Successful in 12s
CI / Makefile Lint (push) Successful in 30s
CI / Unit Tests (push) Successful in 28s
CI / Fuzz Tests (push) Successful in 1m1s
CI / Mutation Tests (push) Successful in 57s
- Added all `Option`-s to the fuzz tests.
  - Minimum load is always <=20%.
- Tested all options, and force a panic on all invalid options.
  - Capacity must now be non-negative.
  - Minimum load should be <=20%, but just put it as a recommendation.

Reviewed-on: #9
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-03-25 01:26:56 +00:00
12 changed files with 150 additions and 45 deletions

View File

@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"Bash(curl -s -w \"\\\\n---HTTP_STATUS:%{http_code}---\" https://raw.githubusercontent.com/kubernetes/kubernetes/master/.markdownlint.yaml)"
]
}
}

View File

@@ -0,0 +1,26 @@
# yaml-language-server: $schema=https://www.schemastore.org/gitea-issue-forms.json
name: 🐛 Bug Report
about: Report a bug in this project
title: "[BUG]: "
body:
- type: textarea
id: context
attributes:
label: Context
placeholder: What circumstances led to the bug?
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
placeholder: What did you expect would happen?
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual Behavior
placeholder: What happened, and why was it unexpected?
validations:
required: true

View File

@@ -0,0 +1,37 @@
# 'Feature Request Template' By @cheehwatang
# https://github.com/cheehwatang/.github/blob/master/.github/ISSUE_TEMPLATE/feature_request.yml
#
# yaml-language-server: $schema=https://www.schemastore.org/gitea-issue-forms.json
name: ✨ Feature Request
about: Suggest an idea for this project
title: "[FEATURE]: "
body:
- type: dropdown
attributes:
multiple: false
label: Feature Type
options:
- "✨ New Feature"
- "📝 Documentation"
- "🎨 Style and UI"
- "🔨 Code Refactor"
- "⚡ Performance Improvements"
- "✅ New Test"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
placeholder: |
Give us a brief description of the feature or enhancement you would
like!
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Additional Information
placeholder: |
Give us some additional information on the feature request like proposed
solutions, links, screenshots, etc.

View File

@@ -0,0 +1,2 @@
# yaml-language-server: $schema=https://www.schemastore.org/gitea-issue-config.json
blank_issues_enabled: false

View File

@@ -0,0 +1,17 @@
---
name: "New Pull Request"
about: "Standard PR template"
title: ""
ref: "main"
---
## Description
## Changes
### Design Decisions
## Checklist
- [ ] Tests pass
- [ ] Docs updated

View File

@@ -6,6 +6,19 @@ on:
pull_request: pull_request:
jobs: jobs:
check-pr-title:
name: Check PR Title
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
env:
TITLE: ${{ gitea.event.pull_request.title }}
steps:
- run: |
if ! echo "$TITLE" | grep -qE '^(WIP: )?(feat|fix|docs|chore|ci|test|refactor|perf|build|style|revert)(\(.+\))?(!)?: .+'; then
echo "::error::Pull Request title must follow conventional commits"
exit 1
fi
lint-go: lint-go:
name: Go Lint name: Go Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest

1
.gitignore vendored
View File

@@ -24,4 +24,3 @@ go.work.sum
# env file # env file
.env .env

View File

@@ -1,7 +1,9 @@
package cuckoo_test package cuckoo_test
import ( import (
"fmt"
"maps" "maps"
"os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -26,6 +28,7 @@ type fuzzStep struct {
type fuzzScenario struct { type fuzzScenario struct {
seedA, seedB uint32 seedA, seedB uint32
capacity, growthFactor uint8
steps []fuzzStep steps []fuzzStep
} }
@@ -40,14 +43,25 @@ func FuzzInsertLookup(f *testing.F) {
return return
} }
if scenario.seedA == scenario.seedB { seedA, seedB := scenario.seedA, scenario.seedB
return 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]( actual := cuckoo.NewCustomTable[uint32, uint32](
offsetHash(scenario.seedA), offsetHash(seedA),
offsetHash(scenario.seedB), offsetHash(seedB),
func(a, b uint32) bool { return a == b }, func(a, b uint32) bool { return a == b },
cuckoo.Capacity(capacity),
cuckoo.GrowthFactor(growthFactor),
) )
expected := map[uint32]uint32{} expected := map[uint32]uint32{}
@@ -59,16 +73,16 @@ func FuzzInsertLookup(f *testing.F) {
delete(expected, step.key) delete(expected, step.key)
_, err = actual.Get(step.key) _, ok := actual.Get(step.key)
assert.Error(err) assert.False(ok)
} else { } else {
err := actual.Put(step.key, step.value) err := actual.Put(step.key, step.value)
assert.NoError(err) assert.NoError(err)
expected[step.key] = step.value expected[step.key] = step.value
found, err := actual.Get(step.key) found, ok := actual.Get(step.key)
assert.NoError(err) assert.True(ok)
assert.Equal(step.value, found) assert.Equal(step.value, found)
} }

View File

@@ -108,12 +108,12 @@ func TestGetMany(t *testing.T) {
} }
for i := range 2_000 { for i := range 2_000 {
value, err := table.Get(i) value, ok := table.Get(i)
if i < 1_000 { if i < 1_000 {
assert.NoError(err) assert.True(ok)
assert.Equal(value, true) assert.Equal(value, true)
} else { } else {
assert.Error(err) assert.False(ok)
} }
} }
} }

View File

@@ -14,19 +14,19 @@ func Example_basic() {
fmt.Println("Put error:", err) fmt.Println("Put error:", err)
} }
if item, err := table.Get(1); err != nil { if item, ok := table.Get(1); !ok {
fmt.Println("Error:", err) fmt.Println("Not Found 1!")
} else { } else {
fmt.Println("Found 1:", item) fmt.Println("Found 1:", item)
} }
if item, err := table.Get(0); err != nil { if item, ok := table.Get(0); !ok {
fmt.Println("Error:", err) fmt.Println("Not Found 0!")
} else { } else {
fmt.Println("Found 0:", item) fmt.Println("Found 0:", item)
} }
// Output: // Output:
// Found 1: Hello, World! // Found 1: Hello, World!
// Error: key '0' not found // Not Found 0!
} }

View File

@@ -1,5 +1,7 @@
package cuckoo package cuckoo
import "fmt"
// DefaultCapacity is the initial capacity of a [Table]. It is inspired from // DefaultCapacity is the initial capacity of a [Table]. It is inspired from
// Java's [HashMap] implementation, which also uses 16. // Java's [HashMap] implementation, which also uses 16.
// //
@@ -10,11 +12,12 @@ const DefaultCapacity uint64 = 16
// hash table implementations use 2. // hash table implementations use 2.
const DefaultGrowthFactor uint64 = 2 const DefaultGrowthFactor uint64 = 2
// DefaultMinimumLoad is the default lowest acceptable occupancy of a [Table]. // defaultMinimumLoad is the default lowest acceptable occupancy of a [Table].
// The value of 5% is taken from [libcuckoo]. // The higher the minimum load, the more likely that a [Table.Put] will not
// succeed. The value of 5% is taken from [libcuckoo].
// //
// [libcuckoo]: https://github.com/efficient/libcuckoo/blob/656714705a055df2b7a605eb3c71586d9da1e119/libcuckoo/cuckoohash_config.hh#L21 // [libcuckoo]: https://github.com/efficient/libcuckoo/blob/656714705a055df2b7a605eb3c71586d9da1e119/libcuckoo/cuckoohash_config.hh#L21
const DefaultMinimumLoad float64 = 0.05 const defaultMinimumLoad float64 = 0.05
type settings struct { type settings struct {
growthFactor uint64 growthFactor uint64
@@ -27,19 +30,21 @@ type settings struct {
type Option func(*settings) type Option func(*settings)
// Capacity modifies the starting capacity of each bucket of the [Table]. The // 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 { func Capacity(value int) Option {
return func(s *settings) { s.bucketSize = uint64(value) } if value < 0 {
} panic(fmt.Sprintf("go-cuckoo: Capacity must be non-negative, got %d", value))
}
// MinimumLoad modifies the [DefaultMinimumLoad] of the [Table]. The value must return func(s *settings) { s.bucketSize = uint64(value) }
// be between 0.00 and 1.00.
func MinimumLoad(value float64) Option {
return func(s *settings) { s.minLoadFactor = value }
} }
// GrowthFactor controls how much the capacity of the [Table] multiplies when // 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 1.
func GrowthFactor(value int) Option { 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) } return func(s *settings) { s.growthFactor = uint64(value) }
} }

View File

@@ -86,24 +86,23 @@ func (t *Table[K, V]) shrink() error {
return t.resize(t.bucketA.capacity / t.growthFactor) return t.resize(t.bucketA.capacity / t.growthFactor)
} }
// Get fetches the value for a key in the [Table]. Returns an error if no value // Get fetches the value for a key in the [Table].
// is found. func (t Table[K, V]) Get(key K) (value V, ok bool) {
func (t Table[K, V]) Get(key K) (value V, err error) {
if item, ok := t.bucketA.get(key); ok { if item, ok := t.bucketA.get(key); ok {
return item, nil return item, true
} }
if item, ok := t.bucketB.get(key); ok { if item, ok := t.bucketB.get(key); ok {
return item, nil return item, true
} }
return value, fmt.Errorf("key '%v' not found", key) return
} }
// Has returns true if a key has a value in the table. // Has returns true if a key has a value in the table.
func (t Table[K, V]) Has(key K) (exists bool) { func (t Table[K, V]) Has(key K) (exists bool) {
_, err := t.Get(key) _, exists = t.Get(key)
return err == nil return
} }
// Put sets the value for a key. Returns error if its value cannot be set. // Put sets the value for a key. Returns error if its value cannot be set.
@@ -198,7 +197,7 @@ func NewCustomTable[K, V any](hashA, hashB Hash[K], compare EqualFunc[K], option
settings := &settings{ settings := &settings{
growthFactor: DefaultGrowthFactor, growthFactor: DefaultGrowthFactor,
bucketSize: DefaultCapacity, bucketSize: DefaultCapacity,
minLoadFactor: DefaultMinimumLoad, minLoadFactor: defaultMinimumLoad,
} }
for _, option := range options { for _, option := range options {