diff --git a/.github/workflows/memkv.yml b/.github/workflows/memkv.yml new file mode 100644 index 0000000..6240a0d --- /dev/null +++ b/.github/workflows/memkv.yml @@ -0,0 +1,134 @@ +name: check-memkv + +on: + push: + branches: + - "main" + paths: + - "memkv/**" + - ".github/workflows/memkv.yml" + pull_request: + branches: + - "*" + paths: + - "memkv/**" + - ".github/workflows/memkv.yml" + +jobs: + + clean: + name: clean + runs-on: ubuntu-latest + timeout-minutes: 2 + strategy: + matrix: + go: [stable] + fail-fast: true + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + cache: true + - name: Run go mod tidy + working-directory: memkv + run: go mod tidy && git diff --exit-code + - name: Run go mod verify + working-directory: memkv + run: go mod verify + - name: Run formatting + working-directory: memkv + run: go run golang.org/x/tools/cmd/goimports@latest -w . && git diff --exit-code + + lint: + name: lint + runs-on: ubuntu-latest + timeout-minutes: 4 + strategy: + matrix: + go: [stable] + fail-fast: true + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + cache: true + - name: Run go linting + uses: golangci/golangci-lint-action@v4 + with: + version: latest + args: --timeout=4m + working-directory: memkv + + test: + name: test + runs-on: ubuntu-latest + timeout-minutes: 2 + strategy: + matrix: + go: [stable] + fail-fast: true + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + cache: true + - name: Run tests + working-directory: memkv + run: go test -shuffle=on -v -count=1 -race -failfast -timeout=30s -covermode=atomic -coverprofile=coverage.out ./... + - name: Codecov Coverage + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + working-directory: memkv + + benchmark: + name: benchmark + runs-on: ubuntu-latest + timeout-minutes: 2 + strategy: + matrix: + go: [stable] + fail-fast: true + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + cache: true + - name: Run benchmarks + working-directory: memkv + run: go test -v -shuffle=on -run=- -bench=. -benchtime=1x -timeout=10s ./... + + build: + name: build + runs-on: ubuntu-latest + timeout-minutes: 4 + strategy: + matrix: + go: [stable] + fail-fast: true + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + cache: true + - name: Run go generate + working-directory: memkv + run: go generate ./... && git diff --exit-code + - name: Run go build + working-directory: memkv + run: go build -o /dev/null ./... diff --git a/memkv/LICENSE b/memkv/LICENSE new file mode 100644 index 0000000..d7af780 --- /dev/null +++ b/memkv/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ben Wakeford + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/memkv/README.md b/memkv/README.md new file mode 100644 index 0000000..5df651a --- /dev/null +++ b/memkv/README.md @@ -0,0 +1,3 @@ +# memkv +[![go reference](https://pkg.go.dev/badge/github.com/wafer-bw/go-toolbox/memkv.svg)](https://pkg.go.dev/github.com/wafer-bw/go-toolbox/memkv) +[![Go Report Card](https://goreportcard.com/badge/github.com/wafer-bw/go-toolbox/memkv)](https://goreportcard.com/report/github.com/wafer-bw/go-toolbox/memkv) diff --git a/memkv/go.mod b/memkv/go.mod new file mode 100644 index 0000000..6995a7a --- /dev/null +++ b/memkv/go.mod @@ -0,0 +1,11 @@ +module github.com/wafer-bw/go-toolbox/memkv + +go 1.22.4 + +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/memkv/go.sum b/memkv/go.sum new file mode 100644 index 0000000..60ce688 --- /dev/null +++ b/memkv/go.sum @@ -0,0 +1,10 @@ +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/memkv/internal/underlying/underlying.go b/memkv/internal/underlying/underlying.go new file mode 100644 index 0000000..346bfe4 --- /dev/null +++ b/memkv/internal/underlying/underlying.go @@ -0,0 +1,15 @@ +// Package underlying provides the underlying data structures for the key-value +// store. +package underlying + +// Item is a wrapper around the instances of data to be stored allowing for +// extensions in the future. +type Item[K comparable, V any] struct { + Value V +} + +// Data is a wrapper around any data types used to store data in the store +// allowing for extensions in the future. +type Data[K comparable, V any] struct { + Items map[K]Item[K, V] +} diff --git a/memkv/memkv.go b/memkv/memkv.go new file mode 100644 index 0000000..dc49015 --- /dev/null +++ b/memkv/memkv.go @@ -0,0 +1,116 @@ +package memkv + +import ( + "sync" + + "github.com/wafer-bw/go-toolbox/memkv/internal/underlying" +) + +type Store[K comparable, V any] struct { // TODO: docstring. + mu *sync.RWMutex + capacity int + data *underlying.Data[K, V] +} + +func New[K comparable, V any](capacity int) *Store[K, V] { // TODO: docstring. + if capacity < 0 { + capacity = 0 + } + + return &Store[K, V]{ + mu: &sync.RWMutex{}, + capacity: capacity, + data: &underlying.Data[K, V]{ + Items: make(map[K]underlying.Item[K, V], capacity), + }, + } +} + +func (s Store[K, V]) Set(key K, val V) error { // TODO: docstring. + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.data.Items[key]; !ok && s.capacity > 0 && len(s.data.Items) >= s.capacity { + return &AtCapacityError{} + } + + s.data.Items[key] = underlying.Item[K, V]{Value: val} + + return nil +} + +func (s Store[K, V]) Get(key K) (V, bool) { // TODO: docstring. + s.mu.RLock() + defer s.mu.RUnlock() + + item, ok := s.data.Items[key] + + return item.Value, ok +} + +func (s Store[K, V]) Delete(keys ...K) { // TODO: docstring. + s.mu.Lock() + defer s.mu.Unlock() + + for _, key := range keys { + delete(s.data.Items, key) + } +} + +func (s Store[K, V]) Flush() { // TODO: docstring. + s.mu.Lock() + defer s.mu.Unlock() + + clear(s.data.Items) +} + +func (s Store[K, V]) Len() int { // TODO: docstring. + s.mu.RLock() + defer s.mu.RUnlock() + + return len(s.data.Items) +} + +func (s Store[K, V]) Items() map[K]V { // TODO: docstring. + s.mu.RLock() + defer s.mu.RUnlock() + + items := make(map[K]V, len(s.data.Items)) + for key, item := range s.data.Items { + items[key] = item.Value + } + + return items +} + +func (s Store[K, V]) Keys() []K { // TODO: docstring. + s.mu.RLock() + defer s.mu.RUnlock() + + keys := make([]K, 0, len(s.data.Items)) + for key := range s.data.Items { + keys = append(keys, key) + } + + return keys +} + +func (s Store[K, V]) Values() []V { // TODO: docstring. + s.mu.RLock() + defer s.mu.RUnlock() + + values := make([]V, 0, len(s.data.Items)) + for _, item := range s.data.Items { + values = append(values, item.Value) + } + + return values +} + +// AtCapcityError occurs when the [Store] is at capacity and new items cannot be +// added. +type AtCapacityError struct{} + +func (e *AtCapacityError) Error() string { + return "store is at capacity" +} diff --git a/memkv/memkv_benchmark_test.go b/memkv/memkv_benchmark_test.go new file mode 100644 index 0000000..51c56cc --- /dev/null +++ b/memkv/memkv_benchmark_test.go @@ -0,0 +1,153 @@ +package memkv_test + +import ( + "fmt" + "testing" + + "github.com/wafer-bw/go-toolbox/memkv" +) + +var sizes = []int{100, 1000, 10000, 100000} + +func BenchmarkStore_Set(b *testing.B) { + for _, size := range sizes { + store := memkv.New[int, int](size) + for i := 0; i < size; i++ { + if err := store.Set(i, i); err != nil { + b.Fatal(err) + } + } + + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + for i := 0; i < b.N; i++ { + if err := store.Set(i%size, i); err != nil { + b.Fatal(err) + } + } + }) + } +} + +func BenchmarkStore_Get(b *testing.B) { + for _, size := range sizes { + store := memkv.New[int, int](size) + for i := 0; i < size; i++ { + if err := store.Set(i, i); err != nil { + b.Fatal(err) + } + } + + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, ok := store.Get(i % size) + _, _ = v, ok + } + }) + } +} + +func BenchmarkStore_Delete(b *testing.B) { + for _, size := range sizes { + store := memkv.New[int, int](size) + for i := 0; i < size; i++ { + if err := store.Set(i, i); err != nil { + b.Fatal(err) + } + } + + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + for i := 0; i < b.N; i++ { + store.Delete(i % size) + } + }) + } +} + +func BenchmarkStore_Flush(b *testing.B) { + for _, size := range sizes { + store := memkv.New[int, int](size) + for i := 0; i < size; i++ { + if err := store.Set(i, i); err != nil { + b.Fatal(err) + } + } + + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + for i := 0; i < b.N; i++ { + store.Flush() + } + }) + } +} + +func BenchmarkStore_Len(b *testing.B) { + for _, size := range sizes { + store := memkv.New[int, int](size) + for i := 0; i < size; i++ { + if err := store.Set(i, i); err != nil { + b.Fatal(err) + } + } + + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + for i := 0; i < b.N; i++ { + l := store.Len() + _ = l + } + }) + } +} + +func BenchmarkStore_Items(b *testing.B) { + for _, size := range sizes { + store := memkv.New[int, int](size) + for i := 0; i < size; i++ { + if err := store.Set(i, i); err != nil { + b.Fatal(err) + } + } + + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + for i := 0; i < b.N; i++ { + items := store.Items() + _ = items + } + }) + } +} + +func BenchmarkStore_Keys(b *testing.B) { + for _, size := range sizes { + store := memkv.New[int, int](size) + for i := 0; i < size; i++ { + if err := store.Set(i, i); err != nil { + b.Fatal(err) + } + } + + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + for i := 0; i < b.N; i++ { + keys := store.Keys() + _ = keys + } + }) + } +} + +func BenchmarkStore_Values(b *testing.B) { + for _, size := range sizes { + store := memkv.New[int, int](size) + for i := 0; i < size; i++ { + if err := store.Set(i, i); err != nil { + b.Fatal(err) + } + } + + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + for i := 0; i < b.N; i++ { + values := store.Values() + _ = values + } + }) + } +} diff --git a/memkv/memkv_example_test.go b/memkv/memkv_example_test.go new file mode 100644 index 0000000..44ddde2 --- /dev/null +++ b/memkv/memkv_example_test.go @@ -0,0 +1,196 @@ +package memkv_test + +import ( + "fmt" + + "github.com/wafer-bw/go-toolbox/memkv" +) + +func ExampleStore_Set() { + store := memkv.New[string, string](0) + + key, val := "key", "val" + if err := store.Set(key, val); err != nil { + fmt.Println(err) + } + + // Output: +} + +func ExampleStore_Get() { + store := memkv.New[string, string](0) + + key, val := "key", "val" + if err := store.Set(key, val); err != nil { + return + } + + v, ok := store.Get(key) + fmt.Println(v, ok) + + // Output: val true +} + +func ExampleStore_Delete() { + store := memkv.New[string, string](0) + + key, val := "key", "val" + if err := store.Set(key, val); err != nil { + return + } + + v, ok := store.Get(key) + fmt.Println(v, ok) + + store.Delete(key) + + v, ok = store.Get(key) + fmt.Println(v, ok) + + // Output: + // val true + // false +} + +func ExampleStore_Delete_multipleKeys() { + store := memkv.New[string, string](0) + + key1, val1 := "key1", "val1" + if err := store.Set(key1, val1); err != nil { + return + } + + key2, val2 := "key2", "val2" + if err := store.Set(key2, val2); err != nil { + return + } + + v, ok := store.Get(key1) + fmt.Println(v, ok) + + v, ok = store.Get(key2) + fmt.Println(v, ok) + + store.Delete(key1, key2) + + v, ok = store.Get(key1) + fmt.Println(v, ok) + + v, ok = store.Get(key2) + fmt.Println(v, ok) + + // Output: + // val1 true + // val2 true + // false + // false +} + +func ExampleStore_Flush() { + store := memkv.New[string, string](0) + + key1, val1 := "key1", "val1" + if err := store.Set(key1, val1); err != nil { + return + } + + key2, val2 := "key2", "val2" + if err := store.Set(key2, val2); err != nil { + return + } + + l := store.Len() + fmt.Println(l) + + store.Flush() + + l = store.Len() + fmt.Println(l) + + // Output: + // 2 + // 0 +} + +func ExampleStore_Len() { + store := memkv.New[string, string](0) + + key, val := "key1", "val1" + if err := store.Set(key, val); err != nil { + return + } + + l := store.Len() + fmt.Println(l) + + if err := store.Set("key2", "val2"); err != nil { + return + } + + l = store.Len() + fmt.Println(l) + + // Output: + // 1 + // 2 +} + +func ExampleStore_Items() { + store := memkv.New[string, string](0) + + key1, val1 := "key1", "val1" + if err := store.Set(key1, val1); err != nil { + return + } + + key2, val2 := "key2", "val2" + if err := store.Set(key2, val2); err != nil { + return + } + + items := store.Items() + fmt.Println(items) + + // Output: + // map[key1:val1 key2:val2] +} + +func ExampleStore_Keys() { + store := memkv.New[string, string](0) + + key1, val1 := "key1", "val1" + if err := store.Set(key1, val1); err != nil { + return + } + + key2, val2 := "key2", "val2" + if err := store.Set(key2, val2); err != nil { + return + } + + keys := store.Keys() + fmt.Println(keys) + + // Output: + // [key1 key2] +} + +func ExampleStore_Values() { + store := memkv.New[string, string](0) + + key1, val1 := "key1", "val1" + if err := store.Set(key1, val1); err != nil { + return + } + + key2, val2 := "key2", "val2" + if err := store.Set(key2, val2); err != nil { + return + } + + values := store.Values() + fmt.Println(values) + + // Output: + // [val1 val2] +} diff --git a/memkv/memkv_export_test.go b/memkv/memkv_export_test.go new file mode 100644 index 0000000..74a5af8 --- /dev/null +++ b/memkv/memkv_export_test.go @@ -0,0 +1,14 @@ +package memkv + +import "github.com/wafer-bw/go-toolbox/memkv/internal/underlying" + +// export for testing. +func (s *Store[K, V]) Capacity() int { + return s.capacity +} + +// export for testing. +func (s *Store[K, V]) Data() (*underlying.Data[K, V], func()) { + s.mu.Lock() + return s.data, s.mu.Unlock +} diff --git a/memkv/memkv_test.go b/memkv/memkv_test.go new file mode 100644 index 0000000..4cad201 --- /dev/null +++ b/memkv/memkv_test.go @@ -0,0 +1,305 @@ +package memkv_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/wafer-bw/go-toolbox/memkv" + "github.com/wafer-bw/go-toolbox/memkv/internal/underlying" +) + +func TestNew(t *testing.T) { + t.Parallel() + + t.Run("creates a new store with no capacity", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + require.Zero(t, store.Capacity()) + }) + + t.Run("creates a new store with a capacity", func(t *testing.T) { + t.Parallel() + + capacity := 10 + store := memkv.New[string, string](capacity) + require.NotNil(t, store) + require.Equal(t, capacity, store.Capacity()) + }) + + t.Run("creates a new store with no capacity when provided a negative capacity", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](-1) + require.NotNil(t, store) + require.Zero(t, store.Capacity()) + }) +} + +func TestStore_Set(t *testing.T) { + t.Parallel() + + t.Run("sets a value in the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + + key, val := "key", "val" + + err := store.Set(key, val) + require.NoError(t, err) + + data, unlock := store.Data() + defer unlock() + require.Len(t, data.Items, 1) + + item, ok := data.Items[key] + require.True(t, ok) + require.Equal(t, val, item.Value) + }) + + t.Run("returns an error when the store is at capacity", func(t *testing.T) { + t.Parallel() + + capacity := 1 + store := memkv.New[string, string](capacity) + require.NotNil(t, store) + + key1, val1 := "key1", "val1" + key2, val2 := "key2", "val2" + + err := store.Set(key1, val1) + require.NoError(t, err) + + err = store.Set(key2, val2) + require.Error(t, err) + require.IsType(t, &memkv.AtCapacityError{}, err) + require.Equal(t, err.Error(), "store is at capacity") + }) +} + +func TestStore_Get(t *testing.T) { + t.Parallel() + + t.Run("gets a value from the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + + key, val := "key", "val" + + data, unlock := store.Data() + data.Items[key] = underlying.Item[string, string]{Value: val} + unlock() + + v, ok := store.Get(key) + require.True(t, ok) + require.Equal(t, val, v) + }) + + t.Run("returns false when the key is not in the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + + _, ok := store.Get("key") + require.False(t, ok) + }) +} + +func TestStore_Delete(t *testing.T) { + t.Parallel() + + t.Run("deletes a value from the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + + key, val := "key", "val" + + data, unlock := store.Data() + data.Items[key] = underlying.Item[string, string]{Value: val} + unlock() + + store.Delete(key) + + data, unlock = store.Data() + defer unlock() + require.Len(t, data.Items, 0) + }) + + t.Run("deletes multiple values from the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + + key1, val1 := "key1", "val1" + key2, val2 := "key2", "val2" + + data, unlock := store.Data() + data.Items[key1] = underlying.Item[string, string]{Value: val1} + data.Items[key2] = underlying.Item[string, string]{Value: val2} + unlock() + + store.Delete(key1, key2) + + data, unlock = store.Data() + defer unlock() + require.Len(t, data.Items, 0) + }) + + t.Run("nothing happens when the keys are not in the store", func(t *testing.T) { + t.Parallel() + + require.NotPanics(t, func() { + store := memkv.New[string, string](0) + require.NotNil(t, store) + + store.Delete("key1", "key2") + }) + }) +} + +func TestStore_Flush(t *testing.T) { + t.Parallel() + + t.Run("flushes the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + + key1, val1 := "key1", "val1" + key2, val2 := "key2", "val2" + + data, unlock := store.Data() + data.Items[key1] = underlying.Item[string, string]{Value: val1} + data.Items[key2] = underlying.Item[string, string]{Value: val2} + unlock() + + store.Flush() + + data, unlock = store.Data() + defer unlock() + require.Len(t, data.Items, 0) + }) + + t.Run("nothing happens when the store is empty", func(t *testing.T) { + t.Parallel() + + require.NotPanics(t, func() { + store := memkv.New[string, string](0) + require.NotNil(t, store) + + store.Flush() + }) + }) +} + +func TestStore_Len(t *testing.T) { + t.Parallel() + + t.Run("returns the number of items in the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + require.Zero(t, store.Len()) + + key1, val1 := "key1", "val1" + key2, val2 := "key2", "val2" + + data, unlock := store.Data() + data.Items[key1] = underlying.Item[string, string]{Value: val1} + data.Items[key2] = underlying.Item[string, string]{Value: val2} + unlock() + + require.Equal(t, 2, store.Len()) + }) +} + +func TestStore_Items(t *testing.T) { + t.Parallel() + + t.Run("returns the items in the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + require.Len(t, store.Items(), 0) + + key1, val1 := "key1", "val1" + key2, val2 := "key2", "val2" + + data, unlock := store.Data() + data.Items[key1] = underlying.Item[string, string]{Value: val1} + data.Items[key2] = underlying.Item[string, string]{Value: val2} + unlock() + + items := store.Items() + require.Len(t, items, 2) + v1, ok := items[key1] + require.True(t, ok) + require.Equal(t, val1, v1) + + v2, ok := items[key2] + require.True(t, ok) + require.Equal(t, val2, v2) + }) +} + +func TestStore_Keys(t *testing.T) { + t.Parallel() + + t.Run("returns the keys in the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + require.Len(t, store.Keys(), 0) + + key1, val1 := "key1", "val1" + key2, val2 := "key2", "val2" + + data, unlock := store.Data() + data.Items[key1] = underlying.Item[string, string]{Value: val1} + data.Items[key2] = underlying.Item[string, string]{Value: val2} + unlock() + + keys := store.Keys() + require.Len(t, keys, 2) + require.Contains(t, keys, key1) + require.Contains(t, keys, key2) + }) +} + +func TestStore_Values(t *testing.T) { + t.Parallel() + + t.Run("returns the values in the store", func(t *testing.T) { + t.Parallel() + + store := memkv.New[string, string](0) + require.NotNil(t, store) + require.Len(t, store.Values(), 0) + + key1, val1 := "key1", "val1" + key2, val2 := "key2", "val2" + + data, unlock := store.Data() + data.Items[key1] = underlying.Item[string, string]{Value: val1} + data.Items[key2] = underlying.Item[string, string]{Value: val2} + unlock() + + values := store.Values() + require.Len(t, values, 2) + require.Contains(t, values, val1) + require.Contains(t, values, val2) + }) +}