Files
grafana/pkg/storage/unified/testing/kv.go
T
Renato Costa 0284d1e669 unified-storage: add UnixTimestamp support to the sqlkv implementation (#115651)
* unified-storage: add `UnixTimestamp` support to sqlkv implementation

* unified-storage: improve tests and enable all of them on sqlkv
2025-12-19 15:35:22 -05:00

952 lines
28 KiB
Go

package test
import (
"bytes"
"context"
"fmt"
"io"
"maps"
"slices"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/util/testutil"
)
// Test names for the KV test suite
const (
TestKVGet = "get operations"
TestKVSave = "save operations"
TestKVDelete = "delete operations"
TestKVKeys = "keys listing"
TestKVKeysWithLimits = "keys with limits and ranges"
TestKVKeysWithSort = "keys with sorting"
TestKVConcurrent = "concurrent operations"
TestKVUnixTimestamp = "unix timestamp"
TestKVBatchGet = "batch get operations"
TestKVBatchDelete = "batch delete operations"
// Use `eventsSection` as the section for the tests, as the sqlkv implementation
// needs a real section to determine which table to use.
testSection = "unified/events"
)
// NewKVFunc is a function that creates a new KV instance for testing
type NewKVFunc func(ctx context.Context) resource.KV
// KVTestOptions configures which tests to run
type KVTestOptions struct {
SkipTests map[string]bool
NSPrefix string // namespace prefix for isolation
}
// GenerateRandomKVPrefix creates a random namespace prefix for test isolation
func GenerateRandomKVPrefix() string {
return fmt.Sprintf("kvtest-%d", time.Now().UnixNano())
}
// RunKVTest runs the KV test suite
func RunKVTest(t *testing.T, newKV NewKVFunc, opts *KVTestOptions) {
if opts == nil {
opts = &KVTestOptions{}
}
if opts.NSPrefix == "" {
opts.NSPrefix = GenerateRandomKVPrefix()
}
t.Logf("Running KV tests with namespace prefix: %s", opts.NSPrefix)
cases := []struct {
name string
fn func(*testing.T, resource.KV, string)
}{
{TestKVGet, runTestKVGet},
{TestKVSave, runTestKVSave},
{TestKVDelete, runTestKVDelete},
{TestKVKeys, runTestKVKeys},
{TestKVKeysWithLimits, runTestKVKeysWithLimits},
{TestKVKeysWithSort, runTestKVKeysWithSort},
{TestKVConcurrent, runTestKVConcurrent},
{TestKVUnixTimestamp, runTestKVUnixTimestamp},
{TestKVBatchGet, runTestKVBatchGet},
{TestKVBatchDelete, runTestKVBatchDelete},
}
for _, tc := range cases {
if shouldSkip := opts.SkipTests[tc.name]; shouldSkip {
t.Logf("Skipping test: %s", tc.name)
continue
}
t.Run(tc.name, func(t *testing.T) {
tc.fn(t, newKV(context.Background()), opts.NSPrefix)
})
}
}
func namespacedKeys(nsPrefix string, keys []string) []string {
prefixed := make([]string, 0, len(keys))
for _, k := range keys {
prefixed = append(prefixed, nsPrefix+"/"+k)
}
return prefixed
}
func namespacedKey(nsPrefix, key string) string {
return namespacedKeys(nsPrefix, []string{key})[0]
}
func runTestKVGet(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-get"
t.Run("get existing key", func(t *testing.T) {
// First save a key
existingKey := namespacedKey(nsPrefix, "existing-key")
testValue := "test value for get"
saveKVHelper(t, kv, ctx, testSection, existingKey, strings.NewReader(testValue))
// Now get it
reader, err := kv.Get(ctx, testSection, existingKey)
require.NoError(t, err)
// Read the value
value, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, testValue, string(value))
// Close the value reader
err = reader.Close()
require.NoError(t, err)
})
t.Run("get non-existent key", func(t *testing.T) {
_, err := kv.Get(ctx, testSection, namespacedKey(nsPrefix, "non-existent-key"))
assert.Error(t, err)
assert.Equal(t, resource.ErrNotFound, err)
})
t.Run("get with empty section", func(t *testing.T) {
_, err := kv.Get(ctx, "", namespacedKey(nsPrefix, "some-key"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "section is required")
})
t.Run("get with empty key", func(t *testing.T) {
_, err := kv.Get(ctx, testSection, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "key is required")
})
}
func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-save"
t.Run("save new key", func(t *testing.T) {
newKey := namespacedKey(nsPrefix, "new-key")
testValue := "new test value"
saveKVHelper(t, kv, ctx, testSection, newKey, strings.NewReader(testValue))
// Verify it was saved
reader, err := kv.Get(ctx, testSection, newKey)
require.NoError(t, err)
value, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, testValue, string(value))
err = reader.Close()
require.NoError(t, err)
})
t.Run("save overwrite existing key", func(t *testing.T) {
overwriteKey := namespacedKey(nsPrefix, "overwrite-key")
// First save
saveKVHelper(t, kv, ctx, testSection, overwriteKey, strings.NewReader("old value"))
// Overwrite
newValue := "new value"
saveKVHelper(t, kv, ctx, testSection, overwriteKey, strings.NewReader(newValue))
// Verify it was updated
reader, err := kv.Get(ctx, testSection, overwriteKey)
require.NoError(t, err)
value, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, newValue, string(value))
err = reader.Close()
require.NoError(t, err)
})
t.Run("save overwrite existing key (datastore)", func(t *testing.T) {
section := "unified/data"
overwriteKey := namespacedKey(nsPrefix, "overwrite-key")
// First save
saveKVHelper(t, kv, ctx, section, overwriteKey, strings.NewReader("old value"))
// Overwrite
newValue := "new value"
saveKVHelper(t, kv, ctx, section, overwriteKey, strings.NewReader(newValue))
// Verify it was updated
reader, err := kv.Get(ctx, section, overwriteKey)
require.NoError(t, err)
value, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, newValue, string(value))
err = reader.Close()
require.NoError(t, err)
})
t.Run("save with empty section", func(t *testing.T) {
_, err := kv.Save(ctx, "", "some-key")
assert.Error(t, err)
assert.Contains(t, err.Error(), "section is required")
})
t.Run("save binary data", func(t *testing.T) {
binaryKey := namespacedKey(nsPrefix, "binary-key")
binaryData := []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD}
saveKVHelper(t, kv, ctx, testSection, binaryKey, bytes.NewReader(binaryData))
// Verify binary data
reader, err := kv.Get(ctx, testSection, binaryKey)
require.NoError(t, err)
value, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, binaryData, value)
err = reader.Close()
require.NoError(t, err)
})
t.Run("save key with no data", func(t *testing.T) {
emptyKey := namespacedKey(nsPrefix, "empty-key")
// Save a key with empty data
saveKVHelper(t, kv, ctx, testSection, emptyKey, strings.NewReader(""))
// Verify it was saved with empty data
reader, err := kv.Get(ctx, testSection, emptyKey)
require.NoError(t, err)
value, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, "", string(value))
assert.Len(t, value, 0)
err = reader.Close()
require.NoError(t, err)
})
}
func runTestKVDelete(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-delete"
t.Run("delete existing key", func(t *testing.T) {
// First create a key
deleteKey := namespacedKey(nsPrefix, "delete-key")
saveKVHelper(t, kv, ctx, testSection, deleteKey, strings.NewReader("delete me"))
// Verify it exists
_, err := kv.Get(ctx, testSection, deleteKey)
require.NoError(t, err)
// Delete it
err = kv.Delete(ctx, testSection, deleteKey)
require.NoError(t, err)
// Verify it's gone
_, err = kv.Get(ctx, testSection, deleteKey)
assert.Error(t, err)
assert.Equal(t, resource.ErrNotFound, err)
})
t.Run("delete non-existent key", func(t *testing.T) {
err := kv.Delete(ctx, testSection, namespacedKey(nsPrefix, "non-existent-delete-key"))
assert.Error(t, err)
assert.Equal(t, resource.ErrNotFound, err)
})
t.Run("delete with empty section", func(t *testing.T) {
err := kv.Delete(ctx, "", namespacedKey(nsPrefix, "some-key"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "section is required")
})
t.Run("delete with empty key", func(t *testing.T) {
err := kv.Delete(ctx, testSection, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "key is required")
})
}
func runTestKVKeys(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-keys"
// Setup test data
testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1"})
for _, key := range testKeys {
saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader("value"+key))
}
t.Run("list all keys", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, testKeys, keys)
})
t.Run("list keys with empty section", func(t *testing.T) {
var keys []string
var errors []error
for k, err := range kv.Keys(ctx, "", resource.ListOptions{}) {
if err != nil {
errors = append(errors, err)
continue
}
keys = append(keys, k)
}
require.Len(t, errors, 1)
assert.Contains(t, errors[0].Error(), "section is required")
assert.Empty(t, keys)
})
t.Run("invalid sort option, defaults to asc", func(t *testing.T) {
var keys []string
var errors []error
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{
Sort: resource.SortOrder(100),
}) {
if err != nil {
errors = append(errors, err)
continue
}
keys = append(keys, k)
}
assert.Empty(t, errors)
assert.Equal(t, testKeys, keys)
})
t.Run("list keys with end key < start key", func(t *testing.T) {
var keys []string
var errors []error
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{
StartKey: namespacedKey(nsPrefix, "c"),
EndKey: namespacedKey(nsPrefix, "a"),
}) {
if err != nil {
errors = append(errors, err)
continue
}
keys = append(keys, k)
}
// Nothing is yielded
assert.Empty(t, errors)
assert.Empty(t, keys)
})
t.Run("list keys returns 0 keys", func(t *testing.T) {
// Use a key range with no keys.
startKey, endKey := "aaaaa", "aaaaz"
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{
StartKey: startKey,
EndKey: endKey,
}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Empty(t, keys)
assert.Len(t, keys, 0)
})
t.Run("interrupting the iterator", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{}) {
require.NoError(t, err)
keys = append(keys, k)
if len(keys) == 2 {
break
}
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"a1", "a2"}), keys)
})
}
func runTestKVKeysWithLimits(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-keys-with-limits"
// Setup test data
testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1", "c2", "d1", "d2"})
for _, key := range testKeys {
saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader("value"+key))
}
t.Run("keys with limit", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{Limit: 3}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"a1", "a2", "b1"}), keys)
})
t.Run("keys with range", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{
StartKey: namespacedKey(nsPrefix, "b"),
EndKey: namespacedKey(nsPrefix, "d"),
}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"b1", "b2", "c1", "c2"}), keys)
})
t.Run("keys with prefix", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{
StartKey: namespacedKey(nsPrefix, "c"),
EndKey: namespacedKey(nsPrefix, resource.PrefixRangeEnd("c")),
}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"c1", "c2"}), keys)
})
t.Run("keys with limit and range", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{
StartKey: namespacedKey(nsPrefix, "a"),
EndKey: namespacedKey(nsPrefix, "c"),
Limit: 2,
}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"a1", "a2"}), keys)
})
}
func runTestKVKeysWithSort(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-keys-with-sort"
// Setup test data
testKeys := namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1"})
for _, key := range testKeys {
saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader("value"+key))
}
t.Run("keys in ascending order (default)", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{Sort: resource.SortOrderAsc}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"a1", "a2", "b1", "b2", "c1"}), keys)
})
t.Run("keys in descending order", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{Sort: resource.SortOrderDesc}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"c1", "b2", "b1", "a2", "a1"}), keys)
})
t.Run("keys descending with prefix", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{
StartKey: namespacedKey(nsPrefix, "a"),
EndKey: namespacedKey(nsPrefix, resource.PrefixRangeEnd("a")),
Sort: resource.SortOrderDesc,
}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"a2", "a1"}), keys)
})
t.Run("keys descending with limit", func(t *testing.T) {
var keys []string
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{
Sort: resource.SortOrderDesc,
Limit: 3,
}) {
require.NoError(t, err)
keys = append(keys, k)
}
assert.Equal(t, namespacedKeys(nsPrefix, []string{"c1", "b2", "b1"}), keys)
})
}
func runTestKVConcurrent(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(60*time.Second))
nsPrefix += "-concurrent"
// Test concurrent operations for both sections, as they have different behaviours
// in the sqlkv implementation.
for _, testSection := range []string{"unified/data", "unified/events"} {
t.Run(testSection, func(t *testing.T) {
t.Run("concurrent save and get operations", func(t *testing.T) {
const numGoroutines = 10
const numOperations = 20
done := make(chan error, numGoroutines)
for goroutineID := range numGoroutines {
go func() {
var err error
defer func() { done <- err }()
for j := range numOperations {
key := namespacedKey(nsPrefix, fmt.Sprintf("concurrent-key-%d-%d", goroutineID, j))
value := fmt.Sprintf("concurrent-value-%d-%d", goroutineID, j)
// Save
writer, err := kv.Save(ctx, testSection, key)
if err != nil {
return
}
defer func() {
err := writer.Close()
require.NoError(t, err)
}()
_, err = io.Copy(writer, strings.NewReader(value))
if err != nil {
return
}
err = writer.Close()
if err != nil {
return
}
// Get immediately
reader, err := kv.Get(ctx, testSection, key)
if err != nil {
return
}
readValue, err := io.ReadAll(reader)
require.NoError(t, err)
err = reader.Close()
require.NoError(t, err)
assert.Equal(t, value, string(readValue))
}
}()
}
// Wait for all goroutines to complete
for range numGoroutines {
err := <-done
require.NoError(t, err)
}
})
t.Run("concurrent save, delete, and list operations", func(t *testing.T) {
const numGoroutines = 5
done := make(chan error, numGoroutines)
for i := range numGoroutines {
go func(goroutineID int) {
var err error
defer func() { done <- err }()
key := namespacedKey(nsPrefix, fmt.Sprintf("concurrent-ops-key-%d", goroutineID))
value := fmt.Sprintf("concurrent-ops-value-%d", goroutineID)
// Save
writer, err := kv.Save(ctx, testSection, key)
if err != nil {
return
}
defer func() {
err := writer.Close()
require.NoError(t, err)
}()
_, err = io.Copy(writer, strings.NewReader(value))
if err != nil {
return
}
err = writer.Close()
if err != nil {
return
}
// List to verify it exists
found := false
for k, err := range kv.Keys(ctx, testSection, resource.ListOptions{}) {
if err != nil {
return
}
if k == key {
found = true
break
}
}
if !found {
err = fmt.Errorf("key %s not found in list", key)
return
}
// Delete
err = kv.Delete(ctx, testSection, key)
if err != nil {
return
}
// Verify it's deleted
_, err = kv.Get(ctx, testSection, key)
require.ErrorIs(t, resource.ErrNotFound, err)
err = nil // Expected error, so clear it
}(i)
}
// Wait for all goroutines to complete
for range numGoroutines {
err := <-done
require.NoError(t, err)
}
})
})
}
}
func runTestKVUnixTimestamp(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
t.Run("unix timestamp returns reasonable value", func(t *testing.T) {
timestamp, err := kv.UnixTimestamp(ctx)
require.NoError(t, err)
now := time.Now().Unix()
// Allow for some time difference (up to 5 seconds)
assert.InDelta(t, now, timestamp, 5)
})
t.Run("unix timestamp is consistent", func(t *testing.T) {
timestamp1, err := kv.UnixTimestamp(ctx)
require.NoError(t, err)
timestamp2, err := kv.UnixTimestamp(ctx)
require.NoError(t, err)
// Should be very close (within 1 second)
require.InDelta(t, timestamp1, timestamp2, 1)
})
}
func runTestKVBatchGet(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-batchget"
t.Run("batch get existing keys", func(t *testing.T) {
// Setup test data
testData := map[string]string{
namespacedKey(nsPrefix, "key1"): "value1",
namespacedKey(nsPrefix, "key2"): "value2",
namespacedKey(nsPrefix, "key3"): "value3",
}
// Save test data
for key, value := range testData {
saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader(value))
}
// Batch get all keys
keys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"})
type result struct {
key string
value string
}
var results []result
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
value, err := io.ReadAll(kv.Value)
require.NoError(t, err)
err = kv.Value.Close()
require.NoError(t, err)
results = append(results, result{key: kv.Key, value: string(value)})
}
// Verify results
require.Len(t, results, 3)
// Check that all keys are present and in order
expectedKeys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"})
actualKeys := make([]string, len(results))
for i, r := range results {
actualKeys[i] = r.key
}
assert.Equal(t, expectedKeys, actualKeys)
// Verify values
for _, r := range results {
assert.Equal(t, testData[r.key], r.value, "key = %s", r.key)
}
})
t.Run("batch get with empty section", func(t *testing.T) {
var kvs []resource.KeyValue
var errs []error
keys := namespacedKeys(nsPrefix, []string{"key1", "key2", "key3"})
for kv, err := range kv.BatchGet(ctx, "", keys) {
if err != nil {
errs = append(errs, err)
continue
}
kvs = append(kvs, kv)
}
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "section is required")
assert.Empty(t, kvs)
})
t.Run("batch get with non-existent keys", func(t *testing.T) {
// Setup some test data
saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "existing-key"), strings.NewReader("existing-value"))
// Batch get with mix of existing and non-existent keys
keys := namespacedKeys(nsPrefix, []string{"existing-key", "non-existent-1", "non-existent-2"})
type result struct {
key string
value string
}
var results []result
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
value, err := io.ReadAll(kv.Value)
require.NoError(t, err)
err = kv.Value.Close()
require.NoError(t, err)
results = append(results, result{key: kv.Key, value: string(value)})
}
// Should only return the existing key
require.Len(t, results, 1)
assert.Equal(t, namespacedKey(nsPrefix, "existing-key"), results[0].key)
assert.Equal(t, "existing-value", results[0].value)
})
t.Run("batch get with all non-existent keys", func(t *testing.T) {
keys := namespacedKeys(nsPrefix, []string{"non-existent-1", "non-existent-2", "non-existent-3"})
var results []resource.KeyValue
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
results = append(results, kv)
}
// Should return no results
assert.Empty(t, results)
})
t.Run("batch get with empty keys list", func(t *testing.T) {
keys := []string{}
var results []resource.KeyValue
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
results = append(results, kv)
}
// Should return no results
assert.Empty(t, results)
})
t.Run("batch get with empty section", func(t *testing.T) {
keys := namespacedKeys(nsPrefix, []string{"some-key"})
var errors []error
for kv, err := range kv.BatchGet(ctx, "", keys) {
if err != nil {
errors = append(errors, err)
continue
}
_ = kv // unused
}
require.Len(t, errors, 1)
assert.Contains(t, errors[0].Error(), "section is required")
})
t.Run("batch get preserves order", func(t *testing.T) {
// Setup test data
testData := map[string]string{
"z-key": "z-value",
"a-key": "a-value",
"m-key": "m-value",
}
// Save test data
for key, value := range testData {
saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, key), strings.NewReader(value))
}
// Batch get in specific order
keys := namespacedKeys(nsPrefix, []string{"z-key", "invalid-key1", "a-key", "invalid-key2", "m-key", "invalid-key3"})
var results []string
for kv, err := range kv.BatchGet(ctx, testSection, keys) {
require.NoError(t, err)
err = kv.Value.Close()
require.NoError(t, err)
results = append(results, kv.Key)
}
// Verify order is preserved
require.Len(t, results, 3)
expectedOrder := namespacedKeys(nsPrefix, []string{"z-key", "a-key", "m-key"})
assert.Equal(t, expectedOrder, results)
})
}
func runTestKVBatchDelete(t *testing.T, kv resource.KV, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
nsPrefix += "-batchdelete"
t.Run("batch delete existing keys", func(t *testing.T) {
// Setup test data
testData := map[string]string{
namespacedKey(nsPrefix, "key1"): "value1",
namespacedKey(nsPrefix, "key2"): "value2",
namespacedKey(nsPrefix, "key3"): "value3",
}
// Save test data
for key, value := range testData {
saveKVHelper(t, kv, ctx, testSection, key, strings.NewReader(value))
}
// Verify keys exist before deletion
for key := range testData {
_, err := kv.Get(ctx, testSection, key)
require.NoError(t, err)
}
// Batch delete all keys
keys := slices.Collect(maps.Keys(testData))
err := kv.BatchDelete(ctx, testSection, keys)
require.NoError(t, err)
// Verify all keys are deleted
for _, key := range keys {
_, err := kv.Get(ctx, testSection, key)
assert.Error(t, err)
assert.Equal(t, resource.ErrNotFound, err)
}
})
t.Run("batch delete with non-existent keys", func(t *testing.T) {
// Setup some test data
key1, key2 := namespacedKey(nsPrefix, "existing-key-1"), namespacedKey(nsPrefix, "existing-key-2")
saveKVHelper(t, kv, ctx, testSection, key1, strings.NewReader("value1"))
saveKVHelper(t, kv, ctx, testSection, key2, strings.NewReader("value2"))
// Batch delete with mix of existing and non-existent keys
keys := []string{key1, namespacedKey(nsPrefix, "non-existent-1"), key2, namespacedKey(nsPrefix, "non-existent-2")}
err := kv.BatchDelete(ctx, testSection, keys)
require.NoError(t, err)
// Verify existing keys are deleted
_, err = kv.Get(ctx, testSection, key1)
require.Error(t, err)
assert.Equal(t, resource.ErrNotFound, err)
_, err = kv.Get(ctx, testSection, key2)
require.Error(t, err)
assert.Equal(t, resource.ErrNotFound, err)
})
t.Run("batch delete with all non-existent keys", func(t *testing.T) {
// Batch delete keys that don't exist
keys := namespacedKeys(nsPrefix, []string{"non-existent-1", "non-existent-2", "non-existent-3"})
err := kv.BatchDelete(ctx, testSection, keys)
require.NoError(t, err)
})
t.Run("batch delete with empty keys list", func(t *testing.T) {
keys := []string{}
err := kv.BatchDelete(ctx, testSection, keys)
require.NoError(t, err)
})
t.Run("batch delete with empty section", func(t *testing.T) {
keys := namespacedKeys(nsPrefix, []string{"some-key"})
err := kv.BatchDelete(ctx, "", keys)
assert.Error(t, err)
assert.Contains(t, err.Error(), "section is required")
})
t.Run("batch delete preserves other keys", func(t *testing.T) {
// Setup test data
saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "keep-key-1"), strings.NewReader("keep-value-1"))
saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "delete-key-1"), strings.NewReader("delete-value-1"))
saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "keep-key-2"), strings.NewReader("keep-value-2"))
saveKVHelper(t, kv, ctx, testSection, namespacedKey(nsPrefix, "delete-key-2"), strings.NewReader("delete-value-2"))
// Batch delete specific keys
keys := namespacedKeys(nsPrefix, []string{"delete-key-1", "delete-key-2"})
err := kv.BatchDelete(ctx, testSection, keys)
require.NoError(t, err)
// Verify deleted keys are gone
_, err = kv.Get(ctx, testSection, namespacedKey(nsPrefix, "delete-key-1"))
assert.Error(t, err)
assert.Equal(t, resource.ErrNotFound, err)
_, err = kv.Get(ctx, testSection, namespacedKey(nsPrefix, "delete-key-2"))
assert.Error(t, err)
assert.Equal(t, resource.ErrNotFound, err)
// Verify kept keys still exist
reader, err := kv.Get(ctx, testSection, namespacedKey(nsPrefix, "keep-key-1"))
require.NoError(t, err)
value, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, "keep-value-1", string(value))
err = reader.Close()
require.NoError(t, err)
reader, err = kv.Get(ctx, testSection, namespacedKey(nsPrefix, "keep-key-2"))
require.NoError(t, err)
value, err = io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, "keep-value-2", string(value))
err = reader.Close()
require.NoError(t, err)
})
}
// saveKVHelper is a helper function to save data to KV store using the new WriteCloser interface
func saveKVHelper(t *testing.T, kv resource.KV, ctx context.Context, section, key string, value io.Reader) {
t.Helper()
writer, err := kv.Save(ctx, section, key)
require.NoError(t, err)
_, err = io.Copy(writer, value)
require.NoError(t, err)
err = writer.Close()
require.NoError(t, err)
}