From a1de40c02ef2b088739d4c47779c865221841cb8 Mon Sep 17 00:00:00 2001 From: TJ Zhang Date: Mon, 9 Dec 2024 17:47:55 -0800 Subject: [PATCH] update tests Signed-off-by: TJ Zhang --- CHANGELOG.md | 1 + go/api/base_client.go | 12 +- go/api/command_options.go | 14 ++ go/api/response_handlers.go | 24 +++- go/api/set_commands.go | 4 +- go/integTest/shared_commands_test.go | 186 ++++++++++++++++++++++++++- 6 files changed, 224 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6606c643e3..020458522b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,7 @@ * Core: Improve retry logic and update unmaintained dependencies for Rust lint CI ([#2673](https://github.com/valkey-io/valkey-glide/pull/2643)) * Core: Release the read lock while creating connections in `refresh_connections` ([#2630](https://github.com/valkey-io/valkey-glide/issues/2630)) * Core: SlotMap refactor - Added NodesMap, Update the slot map upon MOVED errors ([#2682](https://github.com/valkey-io/valkey-glide/issues/2682)) +* Go: Add `SScan` and `SMove` ([#2789](https://github.com/valkey-io/valkey-glide/issues/2789)) #### Breaking Changes diff --git a/go/api/base_client.go b/go/api/base_client.go index 11af988eb1..b3f13baf19 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -644,10 +644,10 @@ func (client *baseClient) SMIsMember(key string, members []string) ([]Result[boo return handleBooleanArrayResponse(result) } -func (client *baseClient) SScan(key string, cursor string) (Result[string], []Result[string], error) { +func (client *baseClient) SScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.SScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -655,16 +655,16 @@ func (client *baseClient) SScan(key string, cursor string) (Result[string], []Re func (client *baseClient) SScanWithOption( key string, cursor string, - options BaseScanOptions, -) (Result[string], []Result[string], error) { + options *BaseScanOptions, +) (string, []string, error) { optionArgs, err := options.toArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.SScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } diff --git a/go/api/command_options.go b/go/api/command_options.go index 46a398c6d2..bbfaf982a0 100644 --- a/go/api/command_options.go +++ b/go/api/command_options.go @@ -298,6 +298,20 @@ type BaseScanOptions struct { count int64 } +func NewBaseScanOptionsBuilder() *BaseScanOptions { + return &BaseScanOptions{} +} + +func (scanOptions *BaseScanOptions) SetMatch(m string) *BaseScanOptions { + scanOptions.match = m + return scanOptions +} + +func (scanOptions *BaseScanOptions) SetCount(c int64) *BaseScanOptions { + scanOptions.count = c + return scanOptions +} + func (opts *BaseScanOptions) toArgs() ([]string, error) { args := []string{} var err error diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 282e7c0860..6bf2105b65 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -380,17 +380,33 @@ type ScanResult struct { func handleScanResponse( response *C.struct_CommandResponse, -) (Result[string], []Result[string], error) { +) (string, []string, error) { defer C.free_command_response(response) slice, err := parseArray(response) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } if arr, ok := slice.([]interface{}); ok { - return arr[0].(Result[string]), arr[1].([]Result[string]), nil + resCollection, err := convertToStrings(arr[1].([]interface{})) + if err != nil { + return "", nil, err + } + return arr[0].(string), resCollection, nil } - return CreateNilStringResult(), nil, err + return "", nil, err +} + +func convertToStrings(input []interface{}) ([]string, error) { + result := make([]string, len(input)) + for i, v := range input { + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf("element at index %d is not a string: %v", i, v) + } + result[i] = str + } + return result, nil } diff --git a/go/api/set_commands.go b/go/api/set_commands.go index 85ac07a143..18f67a32c0 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -341,7 +341,7 @@ type SetCommands interface { // Example: // // [valkey.io]: https://valkey.io/commands/sscan/ - SScan(key string, cursor string) (Result[string], []Result[string], error) + SScan(key string, cursor string) (string, []string, error) // Iterates incrementally over a set. // @@ -360,7 +360,7 @@ type SetCommands interface { // Example: // // [valkey.io]: https://valkey.io/commands/sscan/ - SScanWithOption(key string, cursor string, options BaseScanOptions) (Result[string], []Result[string], error) + SScanWithOption(key string, cursor string, options *BaseScanOptions) (string, []string, error) // Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. // Creates a new destination set if needed. The operation is atomic. diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 88c686e9c5..30e62d2f8d 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -4,6 +4,7 @@ package integTest import ( "math" + "strconv" "time" "github.com/google/uuid" @@ -1826,9 +1827,9 @@ func (suite *GlideTestSuite) TestSMove() { suite.runWithDefaultClients(func(client api.BaseClient) { key1 := "{key}-1-" + uuid.NewString() key2 := "{key}-2-" + uuid.NewString() - // key3 := "{key}-3-" + uuid.NewString() - // stringKey := "{key}-4-" + uuid.NewString() - // nonExistingKey := "{key}-5-" + uuid.NewString() + key3 := "{key}-3-" + uuid.NewString() + stringKey := "{key}-4-" + uuid.NewString() + nonExistingKey := "{key}-5-" + uuid.NewString() memberArray1 := []string{"1", "2", "3"} memberArray2 := []string{"2", "3"} t := suite.T() @@ -1866,18 +1867,193 @@ func (suite *GlideTestSuite) TestSMove() { res7, err := client.SMembers(key1) assert.NoError(t, err) - assert.Len(t, res4, 2) + assert.Len(t, res7, 2) assert.Contains(t, res7, api.CreateStringResult("2")) assert.Contains(t, res7, api.CreateStringResult("3")) res8, err := client.SMembers(key2) assert.NoError(t, err) - assert.Len(t, res4, 2) + assert.Len(t, res8, 2) assert.Contains(t, res8, api.CreateStringResult("1")) assert.Contains(t, res8, api.CreateStringResult("3")) + + // attempt to move from a non-existing key + res9, err := client.SMove(nonExistingKey, key1, "4") + assert.NoError(t, err) + assert.False(t, res9.Value()) + + res10, err := client.SMembers(key1) + assert.NoError(t, err) + assert.Len(t, res10, 2) + assert.Contains(t, res10, api.CreateStringResult("2")) + assert.Contains(t, res10, api.CreateStringResult("3")) + + // move to a new set + res11, err := client.SMove(key1, key3, "2") + assert.NoError(t, err) + assert.True(t, res11.Value()) + + res12, err := client.SMembers(key1) + assert.NoError(t, err) + assert.Len(t, res12, 1) + assert.Contains(t, res12, api.CreateStringResult("3")) + + res13, err := client.SMembers(key3) + assert.NoError(t, err) + assert.Len(t, res13, 1) + assert.Contains(t, res13, api.CreateStringResult("2")) + + // attempt to move a missing element + res14, err := client.SMove(key1, key3, "42") + assert.NoError(t, err) + assert.False(t, res14.Value()) + + res12, err = client.SMembers(key1) + assert.NoError(t, err) + assert.Len(t, res12, 1) + assert.Contains(t, res12, api.CreateStringResult("3")) + + res13, err = client.SMembers(key3) + assert.NoError(t, err) + assert.Len(t, res13, 1) + assert.Contains(t, res13, api.CreateStringResult("2")) + + // moving missing element to missing key + res15, err := client.SMove(key1, nonExistingKey, "42") + assert.NoError(t, err) + assert.False(t, res15.Value()) + + res12, err = client.SMembers(key1) + assert.NoError(t, err) + assert.Len(t, res12, 1) + assert.Contains(t, res12, api.CreateStringResult("3")) + + // key exists but is not contain a set + _, err = client.Set(stringKey, "value") + assert.NoError(t, err) + + _, err = client.SMove(stringKey, key1, "_") + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestSScan() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key1 := "{key}-1-" + uuid.NewString() + key2 := "{key}-2-" + uuid.NewString() + initialCursor := "0" + defaultCount := 10 + // use large dataset to force an iterative cursor. + numMembers := make([]string, 50000) + charMembers := []string{"a", "b", "c", "d", "e"} + t := suite.T() + + // populate the dataset slice + for i := 0; i < 50000; i++ { + numMembers[i] = strconv.Itoa(i) + } + + // empty set + resCursor, resCollection, err := client.SScan(key1, initialCursor) + assert.NoError(t, err) + assert.Equal(t, initialCursor, resCursor) + assert.Empty(t, resCollection) + + // negative cursor + if suite.serverVersion < "8.0.0" { + resCursor, resCollection, err = client.SScan(key1, "-1") + assert.NoError(t, err) + assert.Equal(t, initialCursor, resCursor) + assert.Empty(t, resCollection) + } else { + _, _, err = client.SScan(key1, "-1") + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + } + + // result contains the whole set + res, err := client.SAdd(key1, charMembers) + assert.NoError(t, err) + assert.Equal(t, int64(len(charMembers)), res.Value()) + resCursor, resCollection, err = client.SScan(key1, initialCursor) + assert.NoError(t, err) + assert.Equal(t, initialCursor, resCursor) + assert.Equal(t, len(charMembers), len(resCollection)) + assert.True(t, isSubset(resCollection, charMembers)) + + opts := api.NewBaseScanOptionsBuilder().SetMatch("a") + resCursor, resCollection, err = client.SScanWithOption(key1, initialCursor, opts) + assert.NoError(t, err) + assert.Equal(t, initialCursor, resCursor) + assert.True(t, isSubset(resCollection, []string{"a"})) + + // result contains a subset of the key + res, err = client.SAdd(key1, numMembers) + assert.NoError(t, err) + assert.Equal(t, int64(50000), res.Value()) + resCursor, resCollection, err = client.SScan(key1, "0") + assert.NoError(t, err) + + // 0 is returned for the cursor of the last iteration + for resCursor != "0" { + nextCursor, nextCol, err := client.SScan(key1, resCursor) + assert.NoError(t, err) + assert.NotEqual(t, nextCursor, resCursor) + assert.False(t, isSubset(resCollection, nextCol)) + resCollection = append(resCollection, nextCol...) + resCursor = nextCursor + } + assert.NotEmpty(t, resCollection) + assert.True(t, isSubset(numMembers, resCollection)) + assert.True(t, isSubset(charMembers, resCollection)) + + // test match pattern + opts = api.NewBaseScanOptionsBuilder().SetMatch("*") + resCursor, resCollection, err = client.SScanWithOption(key1, initialCursor, opts) + assert.NoError(t, err) + assert.NotEqual(t, initialCursor, resCursor) + assert.GreaterOrEqual(t, len(resCollection), defaultCount) + + // test count + opts = api.NewBaseScanOptionsBuilder().SetCount(20) + resCursor, resCollection, err = client.SScanWithOption(key1, initialCursor, opts) + assert.NoError(t, err) + assert.NotEqual(t, initialCursor, resCursor) + assert.GreaterOrEqual(t, len(resCollection), 20) + + // test count with match, returns a non-empty array + opts = api.NewBaseScanOptionsBuilder().SetMatch("1*").SetCount(20) + resCursor, resCollection, err = client.SScanWithOption(key1, initialCursor, opts) + assert.NoError(t, err) + assert.NotEqual(t, initialCursor, resCursor) + assert.GreaterOrEqual(t, len(resCollection), 0) + + // exceptions + // non-set key + _, err = client.Set(key2, "test") + assert.NoError(t, err) + + _, _, err = client.SScan(key2, initialCursor) + assert.NotNil(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) }) } +// check if sliceA is a subset of sliceB +func isSubset(sliceA, sliceB []string) bool { + setB := make(map[string]struct{}) + for _, v := range sliceB { + setB[v] = struct{}{} + } + for _, v := range sliceA { + if _, found := setB[v]; !found { + return false + } + } + return true +} + func (suite *GlideTestSuite) TestLRange() { suite.runWithDefaultClients(func(client api.BaseClient) { list := []string{"value4", "value3", "value2", "value1"}