Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,57 @@ jobs:

exit "$failed"

bdd-strict-mode-guard:
name: BDD strict mode guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- name: Verify every godog TestSuite sets Strict=true
run: |
set -euo pipefail

# Locate godog entry files: any .go file that constructs a
# godog.TestSuite{}. We require each one to carry an explicit
# `Strict: true` option. Silently running with strict mode off
# lets unimplemented steps slip through — the single most common
# BDD failure mode and something the project has been burned by.
mapfile -t files < <(grep -rln "godog.TestSuite" --include='*.go' tests || true)

if [ "${#files[@]}" -eq 0 ]; then
echo "::error::No godog.TestSuite entry found — is the BDD suite still present?"
exit 1
fi

failed=0
for f in "${files[@]}"; do
if grep -qE "Strict:\s*false" "$f"; then
echo "::error file=$f::BDD strict mode is disabled (Strict: false). Strict mode is mandatory."
failed=1
continue
fi
if ! grep -qE "Strict:\s*true" "$f"; then
echo "::error file=$f::BDD entry file does not set Strict: true explicitly. The flag MUST be present — do not rely on defaults."
failed=1
continue
fi
echo "$f: OK (Strict: true)"
done

# Forbid any env-var or build-tag escape hatch that would disable
# strict mode conditionally.
if grep -rnE "Strict:\s*[A-Za-z_][A-Za-z0-9_]*" --include='*.go' tests | grep -vE "Strict:\s*true" ; then
echo "::error::A BDD entry file appears to gate Strict on a variable — strict mode MUST be a compile-time constant true."
failed=1
fi

# Flag suspicious skip/pending patterns in feature files.
if grep -rnE "@(skip|wip|pending|todo)\b" --include='*.feature' tests ; then
echo "::error::Feature files contain @skip / @wip / @pending / @todo tags. These are incompatible with strict mode and must be removed before merge."
failed=1
fi

exit "$failed"

attribution-guard:
name: No AI attribution
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ module github.com/axonops/syncmap
go 1.26

require (
github.com/cucumber/godog v0.15.1
github.com/stretchr/testify v1.11.1
go.uber.org/goleak v1.3.0
)

require (
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gofrs/uuid v4.3.1+incompatible // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
47 changes: 43 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,18 +1,57 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI=
github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8=
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
111 changes: 111 additions & 0 deletions syncmap_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2026 AxonOps Limited.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package syncmap_test

import (
"strconv"
"strings"
"sync"
"testing"

"github.com/axonops/syncmap"
)

// FuzzLoadStore verifies that a value stored under a key is always retrievable
// with the correct value and found==true. The fuzz engine varies both the key
// and the value.
func FuzzLoadStore(f *testing.F) {
// Seed corpus
f.Add("", 0)
f.Add("k", 1)
f.Add("\x00key", -1)
f.Add("ü", 2147483647)
f.Add("a very long key "+strings.Repeat("x", 128), -42)

f.Fuzz(func(t *testing.T, k string, v int) {
var m syncmap.SyncMap[string, int]
m.Store(k, v)
got, ok := m.Load(k)
if !ok {
t.Fatalf("Load(%q): expected found=true after Store, got false", k)
}
if got != v {
t.Errorf("Load(%q): expected %d, got %d", k, v, got)
}
})
}

// FuzzConcurrent exercises concurrent Load, Store, Delete, and LoadOrStore
// operations driven by arbitrary byte sequences. It must not panic and must
// be clean under -race.
//
// No ordering assertions are made; the test fails only on panic or a data race
// detected by the race detector.
func FuzzConcurrent(f *testing.F) {
// Seed corpus
f.Add([]byte(""))
f.Add([]byte("\x00"))
f.Add([]byte("\x01\x02\x03\x04"))
f.Add([]byte("\xff\xfe\xfd\x00\x01\x02\x03"))

f.Fuzz(func(t *testing.T, data []byte) {
if len(data) == 0 {
return
}

var m syncmap.SyncMap[string, int]

// Distribute the byte slice across 4 goroutines. Each goroutine processes
// its own quarter of the data so the workload genuinely exercises
// concurrent access without ordering assumptions.
const numGoroutines = 4
chunkSize := (len(data) + numGoroutines - 1) / numGoroutines

var wg sync.WaitGroup
for g := 0; g < numGoroutines; g++ {
start := g * chunkSize
if start >= len(data) {
break
}
end := start + chunkSize
if end > len(data) {
end = len(data)
}
chunk := data[start:end]

wg.Add(1)
go func(chunk []byte) {
defer wg.Done()
for _, b := range chunk {
op := b % 4
key := strconv.Itoa(int(b) % 8)
value := int(b)
switch op {
case 0:
m.Load(key)
case 1:
m.Store(key, value)
case 2:
m.Delete(key)
case 3:
m.LoadOrStore(key, value)
}
}
}(chunk)
}

wg.Wait()
})
}
54 changes: 54 additions & 0 deletions tests/bdd/bdd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2026 AxonOps Limited.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build bdd

package bdd_test

import (
"os"
"testing"

"github.com/cucumber/godog"
"github.com/cucumber/godog/colors"

"github.com/axonops/syncmap/tests/bdd/steps"
)

// TestGodog is the single entry point for the godog BDD suite. It runs every
// .feature file under tests/bdd/features.
//
// Strict mode is MANDATORY and MUST NOT be disabled. When Strict is true,
// godog fails the suite on any undefined or pending step — silently
// skipping unimplemented fixtures is the single most common BDD failure
// mode and we refuse to let it past CI. The CI workflow carries a guard
// job that greps every BDD entry file for the Strict flag and fails the
// build if the flag is missing or set to false. See
// .github/workflows/ci.yml → bdd-strict-mode-guard.
func TestGodog(t *testing.T) {
suite := godog.TestSuite{
ScenarioInitializer: steps.Register,
Options: &godog.Options{
Format: "pretty",
Paths: []string{"features"},
Output: colors.Colored(os.Stdout),
Randomize: -1,
Strict: true,
TestingT: t,
},
}
if got := suite.Run(); got != 0 {
t.Fatalf("godog suite exited with %d", got)
}
}
Loading