diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..dd8b384 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/devcontainers-extra/features/mise:1": { + "version": "1.0.0", + "resolved": "ghcr.io/devcontainers-extra/features/mise@sha256:5c389ab1e3c8e44e5e0ff119f6868d54dbb781bc860bd8587dc66209cb8c4a8c", + "integrity": "sha256:5c389ab1e3c8e44e5e0ff119f6868d54dbb781bc860bd8587dc66209cb8c4a8c" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d134ae2..e7d3c58 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,4 @@ - // For format details, see https://aka.ms/devcontainer.json. +// For format details, see https://aka.ms/devcontainer.json. { "name": "Ubuntu", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile @@ -6,13 +6,15 @@ // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers-extra/features/mise:1": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": {} + "ghcr.io/devcontainers/features/github-cli:1": {} }, "postCreateCommand": "bash .devcontainer/post_create.sh", "customizations": { "vscode": { "extensions": [ - "golang.go" + "golang.go", + "hverlin.mise-vscode", + "tamasfe.even-better-toml" ] } } diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh index a190145..ec19e0a 100644 --- a/.devcontainer/post_create.sh +++ b/.devcontainer/post_create.sh @@ -1,9 +1,9 @@ #!/bin/bash -# note that bash will read from ~/.profile or ~/.bash_profile if the latter exists -# ergo, you may want to check to see which is defined on your system and only append to the existing file -echo 'eval "$(mise activate bash --shims)"' >>~/.bash_profile # this sets up non-interactive sessions -echo 'eval "$(mise activate bash)"' >>~/.bashrc # this sets up interactive sessions +set -euo pipefail + +echo 'eval "$(mise activate bash --shims)"' >>~/.bash_profile +echo 'eval "$(mise activate bash)"' >>~/.bashrc mise trust . @@ -13,3 +13,7 @@ mise exec -- go mod download mise exec -- go install -v golang.org/x/tools/gopls@latest mise exec -- go install -v github.com/go-delve/delve/cmd/dlv@latest + +if [[ -f ".devcontainer/post_create.local.sh" ]]; then + source ".devcontainer/post_create.local.sh" +fi diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 820bfcf..50598df 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -3,49 +3,99 @@ # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. -name: "CodeQL" +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" on: push: - branches: - - main + branches: [ "main" ] pull_request: - # The branches below must be a subset of the branches above - branches: - - main + branches: [ "main" ] schedule: - cron: "0 17 * * 5" jobs: analyze: - name: Analyze - runs-on: ubuntu-latest - + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: # required for all workflows security-events: write + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + strategy: fail-fast: false matrix: - # Override automatic language detection by changing the below list - # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] - # TODO: Enable for javascript later - language: ["go"] - + include: + - language: actions + build-mode: none + - language: go + build-mode: autobuild + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v5 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + - name: Checkout repository + uses: actions/checkout@v6 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 29e78aa..0580ebb 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -5,10 +5,18 @@ on: branches: - main +permissions: + contents: read + +concurrency: + group: pr-check-${{ github.ref }} + cancel-in-progress: true + jobs: check: name: Check runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 @@ -16,7 +24,23 @@ jobs: fetch-depth: 0 - name: Install mise - uses: jdx/mise-action@v4 + uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 + + - name: Cache Go modules and build + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }} + restore-keys: go-${{ runner.os }}- + + - name: Cache golangci-lint + uses: actions/cache@v4 + with: + path: ~/.cache/golangci-lint + key: golangci-lint-${{ runner.os }}-${{ hashFiles('mise.toml', '.golangci.yaml') }} + restore-keys: golangci-lint-${{ runner.os }}- - name: Download Go dependencies run: go mod download diff --git a/.gitignore b/.gitignore index 9c05bdc..3f90277 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ # go work go.work go.work.sum + +# Local configuration files +*.local.* +*.local/ +*.local diff --git a/.golangci.yaml b/.golangci.yaml index eb3c803..16a0424 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -10,10 +10,6 @@ issues: severity: default: error - # rules: - # - linters: - # - dupl - # severity: error formatters: enable: @@ -22,7 +18,7 @@ formatters: - gofumpt - goimports - golines - - swaggo + settings: gofmt: rewrite-rules: @@ -30,8 +26,8 @@ formatters: replacement: "any" gofumpt: extra-rules: true - exclusions: - paths: [] + golines: + max-len: 120 linters: default: none @@ -45,33 +41,66 @@ linters: # Additional - bodyclose + - canonicalheader + - copyloopvar - depguard + - dupword + - durationcheck + - errchkjson - errname - errorlint + - exptostd + - fatcontext + - gocheckcompilerdirectives - gocognit - goconst - - goheader - - gomodguard + - gocritic + - gomodguard_v2 - goprintffuncname - gosec + - intrange + - loggercheck + - mirror + - misspell + - modernize + - musttag - nakedret + - nestif - nilerr + - nilnesserr - nilnil - noctx - nolintlint + - nosprintfhostport + - perfsprint - prealloc - promlinter + - recvcheck - revive + - rowserrcheck + - sloglint + - spancheck + - sqlclosecheck - testifylint - testpackage - thelper + - unconvert + - unparam - usestdlibvars + - usetesting - wastedassign + - whitespace - wsl_v5 - disable: [] - settings: + govet: + enable: + - nilness + + modernize: + disable: + - omitzero + depguard: rules: main: @@ -84,16 +113,3 @@ linters: desc: Use github.com/go-resty/resty/v2 instead - pkg: github.com/aws/smithy-go/ptr$ desc: Use github.com/aws/aws-sdk-go-v2/aws instead - govet: - enable: - - nilness - - # exclusions: - # paths: - # - (.+)_test\.go - # rules: - # - path: (.+)_test\.go - # linters: - # - dupl - # - mnd - # - lll diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..01498bd --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ms-vscode-remote.remote-containers", + "golang.go", + "hverlin.mise-vscode", + "tamasfe.even-better-toml" + ] +} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 11adccf..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,46 +0,0 @@ -# Agent Guidelines - -This repository provides `gsyncmap` — a generic, type-safe wrapper around [`sync.Map`](https://pkg.go.dev/sync#Map) for Go. - -## Repository Structure - -| File | Description | -|------|-------------| -| `map.go` | `Map[Key, Value]` — generic concurrent map, value type can be `any` | -| `comparable_map.go` | `ComparableMap[Key, Value]` — variant requiring `comparable` value type | -| `map_test.go` | Unit tests | -| `map_example_test.go` | Runnable example tests (shown in pkg.go.dev) | - -## Development Commands - -```sh -# Run tests (with race detector) -make test -# go test -v -race -failfast ./... - -# Run linter -make lint -# golangci-lint run -v ./... - -# Run linter with auto-fix -make fix -# golangci-lint run -v --fix ./... - -# Run all checks -make check -``` - -## Code Conventions - -- **Go version**: 1.24+, module path `github.com/min0625/gsyncmap` -- All exported types and methods must have doc comments. -- `Map` wraps `sync.Map` via a direct type alias (`type Map[K, V] sync.Map`); avoid adding fields. -- `CompareAndDelete` / `CompareAndSwap` on `Map` are intentionally **deprecated** — do not un-deprecate them. Direct users to `ComparableMap` instead. -- Keep the two types (`Map` and `ComparableMap`) in sync: any new method added to one should be added to the other when applicable. -- New example functions must follow `Example*` naming conventions and include expected output comments so they serve as testable examples. - -## Testing Guidelines - -- All tests must pass with `-race` flag enabled. -- Cover both the normal (value found) and zero-value (key absent) code paths for every method. -- Do not add external test dependencies; use only the standard library. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..09824ef --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# Agent Guidelines + +This repository provides `gsyncmap` — a generic, type-safe wrapper around [`sync.Map`](https://pkg.go.dev/sync#Map) for Go. + +## Repository Structure + +| File | Description | +|------|-------------| +| `map.go` | `Map[Key, Value]` — generic concurrent map, value type can be `any` | +| `comparable_map.go` | `ComparableMap[Key, Value]` — variant requiring `comparable` value type | +| `map_test.go` | Unit tests | +| `map_example_test.go` | Runnable example tests (shown in pkg.go.dev) | + +## Development Commands + +```sh +# Run tests (with race detector) +make test +# go test -race -failfast ./... + +# Run linter (verifies config, then lints) +make lint +# golangci-lint config verify +# golangci-lint run --new-from-rev=HEAD ./... + +# Run linter with auto-fix +make fix +# go mod tidy +# golangci-lint run --new-from-rev=HEAD --fix ./... + +# Check that go.mod/go.sum are tidy +make check-tidy +# go mod tidy -diff + +# Run all checks (check-tidy + lint + test) +make check +``` + +> **Note:** `lint`/`fix` use `--new-from-rev=$(NEW_FROM_REV)` (default `HEAD`), so they +> only report issues on changed lines. To lint the whole tree, pass an empty revision, +> e.g. `make lint NEW_FROM_REV=`. + +Tool versions are pinned in `mise.toml` (see it for the exact Go and golangci-lint +versions). The linter uses the golangci-lint v2 config schema (`.golangci.yaml`, +`version: "2"`). + +## Code Conventions + +- **Go version**: see the `go` directive in `go.mod` (toolchain pinned in `mise.toml`); module path `github.com/min0625/gsyncmap` +- All exported types and methods must have doc comments. +- `Map` is a defined type over `sync.Map` (`type Map[Key comparable, Value any] sync.Map`), accessed through the unexported `syncMap()` helper; do not add fields. The unexported `zero[T]()` helper returns the zero `Value` on miss. +- `ComparableMap[Key, Value comparable]` embeds `Map` and only overrides `CompareAndDelete` / `CompareAndSwap` with panic-free implementations; all other methods are inherited. +- `CompareAndDelete` / `CompareAndSwap` on `Map` are intentionally **deprecated** — do not un-deprecate them. Direct users to `ComparableMap` instead. +- Keep the two types in sync: any new method added to `Map` should be considered for `ComparableMap` (and vice versa) when applicable. +- New example functions must follow `Example*` naming conventions and include expected output comments so they serve as testable examples. + +## Testing Guidelines + +- All tests must pass with `-race` flag enabled. +- Cover both the normal (value found) and zero-value (key absent) code paths for every method. +- Do not add external test dependencies; use only the standard library. diff --git a/Makefile b/Makefile index 78bf91e..d7d3ae0 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,22 @@ NEW_FROM_REV ?= HEAD +.PHONY: fix fix: go mod tidy - golangci-lint run -v --new-from-rev=$(NEW_FROM_REV) --fix ./... + golangci-lint run --new-from-rev=$(NEW_FROM_REV) --fix ./... +.PHONY: lint lint: - golangci-lint run -v --new-from-rev=$(NEW_FROM_REV) ./... + golangci-lint config verify + golangci-lint run --new-from-rev=$(NEW_FROM_REV) ./... +.PHONY: test test: - go test -v -race -failfast ./... + go test -race -failfast ./... +.PHONY: check-tidy check-tidy: go mod tidy -diff +.PHONY: check check: check-tidy lint test diff --git a/README.md b/README.md index 2a9124e..d742551 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,40 @@ # Generic Sync Map + [![Go Reference](https://pkg.go.dev/badge/github.com/min0625/gsyncmap.svg)](https://pkg.go.dev/github.com/min0625/gsyncmap) -A generic, type-safe wrapper around [`sync.Map`](https://pkg.go.dev/sync#Map). +**English** | [繁體中文](./README.zh-TW.md) + +A generic, type-safe wrapper around [`sync.Map`](https://pkg.go.dev/sync#Map) for Go. + +## Features + +- **Type-safe** — keys and values are statically typed; no `any` casts at call sites. +- **Zero-value ready** — the zero value is usable immediately, just like `sync.Map`. +- **Familiar API** — mirrors the standard `sync.Map` method set. +- **No dependencies** — built entirely on the standard library. ## Installation + ```sh go get github.com/min0625/gsyncmap ``` +Requires Go 1.24 or later. + ## Types +| Type | Value constraint | `CompareAndDelete` / `CompareAndSwap` | +|------|------------------|----------------------------------------| +| [`Map[Key comparable, Value any]`](./map.go) | `any` | available but **deprecated** (may panic at runtime) | +| [`ComparableMap[Key, Value comparable]`](./comparable_map.go) | `comparable` | safe, panic-free | + ### `Map[Key comparable, Value any]` A generic concurrent map that accepts any value type. Suitable for most use cases. -> **Note:** `CompareAndDelete` and `CompareAndSwap` are available on `Map` but deprecated. -> They may panic at runtime if `Value` is not a comparable type (e.g. slice, map, func). -> Use `ComparableMap` instead when these operations are needed. +> **Note:** `CompareAndDelete` and `CompareAndSwap` are available on `Map` but +> deprecated — they panic at runtime if `Value` is not comparable (e.g. slice, +> map, func). Use `ComparableMap` when these operations are needed. ### `ComparableMap[Key, Value comparable]` @@ -62,10 +80,15 @@ func main() { m.Store("k1", "v1") fmt.Println(m.CompareAndSwap("k1", "v1", "v2")) // true - fmt.Println(m.Load("k1")) // v2 true + fmt.Println(m.Load("k1")) // v2 true } ``` -## Examples -See: [map_example_test.go](./map_example_test.go) +## Documentation + +- API reference: [pkg.go.dev/github.com/min0625/gsyncmap](https://pkg.go.dev/github.com/min0625/gsyncmap) +- Runnable examples: [map_example_test.go](./map_example_test.go) + +## License +See [LICENSE](./LICENSE). diff --git a/README.zh-TW.md b/README.zh-TW.md new file mode 100644 index 0000000..bdd2e86 --- /dev/null +++ b/README.zh-TW.md @@ -0,0 +1,94 @@ +# Generic Sync Map + +[![Go Reference](https://pkg.go.dev/badge/github.com/min0625/gsyncmap.svg)](https://pkg.go.dev/github.com/min0625/gsyncmap) + +[English](./README.md) | **繁體中文** + +針對 Go 的 [`sync.Map`](https://pkg.go.dev/sync#Map) 所打造、泛型且型別安全的封裝。 + +## 特色 + +- **型別安全** — 鍵與值皆為靜態型別;呼叫端不需要 `any` 型別轉換。 +- **零值即可用** — 零值可立即使用,與 `sync.Map` 一致。 +- **熟悉的 API** — 對應標準函式庫 `sync.Map` 的方法集。 +- **零相依** — 完全建構於標準函式庫之上。 + +## 安裝 + +```sh +go get github.com/min0625/gsyncmap +``` + +需要 Go 1.24 或更新版本。 + +## 型別 + +| 型別 | 值的限制 | `CompareAndDelete` / `CompareAndSwap` | +|------|----------|----------------------------------------| +| [`Map[Key comparable, Value any]`](./map.go) | `any` | 可用但**已棄用**(執行期可能 panic) | +| [`ComparableMap[Key, Value comparable]`](./comparable_map.go) | `comparable` | 安全、不會 panic | + +### `Map[Key comparable, Value any]` + +接受任意值型別的泛型並行 map,適用於大多數情境。 + +> **注意:** `Map` 上的 `CompareAndDelete` 與 `CompareAndSwap` 雖然可用,但已棄用 +> ——當 `Value` 不是 comparable(例如 slice、map、func)時,會在執行期 panic。 +> 需要這些操作時請改用 `ComparableMap`。 + +### `ComparableMap[Key, Value comparable]` + +可直接替換 `Map` 的版本,但要求值型別為 comparable。 +提供安全的 `CompareAndDelete` 與 `CompareAndSwap`,不會有執行期 panic 的風險。 + +## 快速開始 + +```go +package main + +import ( + "fmt" + + "github.com/min0625/gsyncmap" +) + +func main() { + var m gsyncmap.Map[string, string] + + m.Store("k1", "v1") + fmt.Println(m.Load("k1")) // v1 true + fmt.Println(m.Load("k2")) // false + + m.Delete("k1") + fmt.Println(m.Load("k1")) // false +} +``` + +當需要 `CompareAndDelete` 或 `CompareAndSwap` 時,請改用 `ComparableMap`: + +```go +package main + +import ( + "fmt" + + "github.com/min0625/gsyncmap" +) + +func main() { + var m gsyncmap.ComparableMap[string, string] + + m.Store("k1", "v1") + fmt.Println(m.CompareAndSwap("k1", "v1", "v2")) // true + fmt.Println(m.Load("k1")) // v2 true +} +``` + +## 文件 + +- API 參考文件:[pkg.go.dev/github.com/min0625/gsyncmap](https://pkg.go.dev/github.com/min0625/gsyncmap) +- 可執行範例:[map_example_test.go](./map_example_test.go) + +## 授權 + +請參閱 [LICENSE](./LICENSE)。 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/map_example_test.go b/map_example_test.go index ddfa2e8..b5aeb53 100644 --- a/map_example_test.go +++ b/map_example_test.go @@ -92,6 +92,27 @@ func ExampleMap_Range_break() { // 1 } +func ExampleComparableMap() { + var m gsyncmap.ComparableMap[string, int] + + m.Store("k1", 1) + fmt.Println(m.Load("k1")) + + // CompareAndSwap and CompareAndDelete are panic-free because the value + // type is constrained to comparable. + fmt.Println(m.CompareAndSwap("k1", 1, 2)) + fmt.Println(m.Load("k1")) + fmt.Println(m.CompareAndDelete("k1", 2)) + fmt.Println(m.Load("k1")) + + // Output: + // 1 true + // true + // 2 true + // true + // 0 false +} + func ExampleComparableMap_CompareAndDelete() { var m gsyncmap.ComparableMap[string, string] diff --git a/map_test.go b/map_test.go index d06a9da..47ee9c2 100644 --- a/map_test.go +++ b/map_test.go @@ -267,6 +267,56 @@ func TestMap_CompareAndSwap_Deprecated(t *testing.T) { } } +func TestMap_NonComparableValue(t *testing.T) { + t.Parallel() + + // Map accepts non-comparable value types (e.g. slices) for every method + // except the deprecated CompareAndDelete/CompareAndSwap. + var m gsyncmap.Map[string, []int] + + // Load absent key returns the zero value (nil slice) and false. + v, ok := m.Load("missing") + if ok || v != nil { + t.Fatalf("Load missing key: got (%v, %v), want (nil, false)", v, ok) + } + + m.Store("k", []int{1, 2, 3}) + + v, ok = m.Load("k") + if !ok || len(v) != 3 || v[0] != 1 || v[1] != 2 || v[2] != 3 { + t.Fatalf("Load after Store: got (%v, %v), want ([1 2 3], true)", v, ok) + } + + // LoadOrStore on an existing key returns the stored slice. + actual, loaded := m.LoadOrStore("k", []int{9}) + if !loaded || len(actual) != 3 { + t.Fatalf("LoadOrStore existing: got (%v, %v), want ([1 2 3], true)", actual, loaded) + } + + // Range visits the stored entry. + var visited int + + m.Range(func(key string, value []int) bool { + visited++ + + if key != "k" || len(value) != 3 { + t.Errorf("Range: got (%q, %v), want (\"k\", [1 2 3])", key, value) + } + + return true + }) + + if visited != 1 { + t.Fatalf("Range visited %d entries, want 1", visited) + } + + m.Delete("k") + + if _, ok := m.Load("k"); ok { + t.Fatal("key should be deleted") + } +} + // --- ComparableMap tests --- func TestComparableMap_BasicOperations(t *testing.T) { @@ -372,6 +422,53 @@ func TestComparableMap_LoadOrStore(t *testing.T) { } } +func TestComparableMap_Swap(t *testing.T) { + t.Parallel() + + var m gsyncmap.ComparableMap[string, int] + + // Key absent: previous is zero, loaded=false. + prev, loaded := m.Swap("k", 1) + if loaded || prev != 0 { + t.Fatalf("Swap missing key: got (%v, %v), want (0, false)", prev, loaded) + } + + // Key present: previous is old value, loaded=true. + prev, loaded = m.Swap("k", 2) + if !loaded || prev != 1 { + t.Fatalf("Swap existing key: got (%v, %v), want (1, true)", prev, loaded) + } + + v, _ := m.Load("k") + if v != 2 { + t.Fatalf("Load after Swap: got %v, want 2", v) + } +} + +func TestComparableMap_LoadAndDelete(t *testing.T) { + t.Parallel() + + var m gsyncmap.ComparableMap[string, int] + + // Key absent: returns zero value and false. + v, loaded := m.LoadAndDelete("missing") + if loaded || v != 0 { + t.Fatalf("LoadAndDelete missing: got (%v, %v), want (0, false)", v, loaded) + } + + m.Store("k", 7) + + v, loaded = m.LoadAndDelete("k") + if !loaded || v != 7 { + t.Fatalf("LoadAndDelete existing: got (%v, %v), want (7, true)", v, loaded) + } + + _, ok := m.Load("k") + if ok { + t.Fatal("key should be absent after LoadAndDelete") + } +} + func TestComparableMap_Clear(t *testing.T) { t.Parallel() diff --git a/mise.toml b/mise.toml index 42162e8..11e03a6 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,3 @@ [tools] go = "1.24.13" -golangci-lint = "2.11.2" +golangci-lint = "2.12.2"