mirror of
https://github.com/EchoVault/SugarDB.git
synced 2025-10-05 16:06:57 +08:00
Removed test folder and moved all commands tests to their respective internal modules. Moved api tests into echovault package. This change has been made because the speratate test folder is not idiomatic and caused test coverage report to not be generated.
This commit is contained in:
472
internal/modules/string/commands_test.go
Normal file
472
internal/modules/string/commands_test.go
Normal file
@@ -0,0 +1,472 @@
|
||||
// Copyright 2024 Kelvin Clement Mwinuka
|
||||
//
|
||||
// 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 str_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/echovault/echovault/echovault"
|
||||
"github.com/echovault/echovault/internal"
|
||||
"github.com/echovault/echovault/internal/config"
|
||||
"github.com/echovault/echovault/internal/constants"
|
||||
"github.com/tidwall/resp"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var mockServer *echovault.EchoVault
|
||||
|
||||
func init() {
|
||||
mockServer, _ = echovault.NewEchoVault(
|
||||
echovault.WithConfig(config.Config{
|
||||
DataDir: "",
|
||||
EvictionPolicy: constants.NoEviction,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func getUnexportedField(field reflect.Value) interface{} {
|
||||
return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface()
|
||||
}
|
||||
|
||||
func getHandler(commands ...string) internal.HandlerFunc {
|
||||
if len(commands) == 0 {
|
||||
return nil
|
||||
}
|
||||
getCommands :=
|
||||
getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command)
|
||||
for _, c := range getCommands() {
|
||||
if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 {
|
||||
// Get command handler
|
||||
return c.HandlerFunc
|
||||
}
|
||||
if strings.EqualFold(commands[0], c.Command) {
|
||||
// Get sub-command handler
|
||||
for _, sc := range c.SubCommands {
|
||||
if strings.EqualFold(commands[1], sc.Command) {
|
||||
return sc.HandlerFunc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams {
|
||||
return internal.HandlerFuncParams{
|
||||
Context: ctx,
|
||||
Command: cmd,
|
||||
Connection: conn,
|
||||
KeyExists: mockServer.KeyExists,
|
||||
CreateKeyAndLock: mockServer.CreateKeyAndLock,
|
||||
KeyLock: mockServer.KeyLock,
|
||||
KeyRLock: mockServer.KeyRLock,
|
||||
KeyUnlock: mockServer.KeyUnlock,
|
||||
KeyRUnlock: mockServer.KeyRUnlock,
|
||||
GetValue: mockServer.GetValue,
|
||||
SetValue: mockServer.SetValue,
|
||||
}
|
||||
}
|
||||
|
||||
func Test_HandleSetRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
preset bool
|
||||
key string
|
||||
presetValue string
|
||||
command []string
|
||||
expectedValue string
|
||||
expectedResponse int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Test that SETRANGE on non-existent string creates new string",
|
||||
preset: false,
|
||||
key: "SetRangeKey1",
|
||||
presetValue: "",
|
||||
command: []string{"SETRANGE", "SetRangeKey1", "10", "New String Value"},
|
||||
expectedValue: "New String Value",
|
||||
expectedResponse: len("New String Value"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Test SETRANGE with an offset that leads to a longer resulting string",
|
||||
preset: true,
|
||||
key: "SetRangeKey2",
|
||||
presetValue: "Original String Value",
|
||||
command: []string{"SETRANGE", "SetRangeKey2", "16", "Portion Replaced With This New String"},
|
||||
expectedValue: "Original String Portion Replaced With This New String",
|
||||
expectedResponse: len("Original String Portion Replaced With This New String"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "SETRANGE with negative offset prepends the string",
|
||||
preset: true,
|
||||
key: "SetRangeKey3",
|
||||
presetValue: "This is a preset value",
|
||||
command: []string{"SETRANGE", "SetRangeKey3", "-10", "Prepended "},
|
||||
expectedValue: "Prepended This is a preset value",
|
||||
expectedResponse: len("Prepended This is a preset value"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "SETRANGE with offset that embeds new string inside the old string",
|
||||
preset: true,
|
||||
key: "SetRangeKey4",
|
||||
presetValue: "This is a preset value",
|
||||
command: []string{"SETRANGE", "SetRangeKey4", "0", "That"},
|
||||
expectedValue: "That is a preset value",
|
||||
expectedResponse: len("That is a preset value"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "SETRANGE with offset longer than original lengths appends the string",
|
||||
preset: true,
|
||||
key: "SetRangeKey5",
|
||||
presetValue: "This is a preset value",
|
||||
command: []string{"SETRANGE", "SetRangeKey5", "100", " Appended"},
|
||||
expectedValue: "This is a preset value Appended",
|
||||
expectedResponse: len("This is a preset value Appended"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "SETRANGE with offset on the last character replaces last character with new string",
|
||||
preset: true,
|
||||
key: "SetRangeKey6",
|
||||
presetValue: "This is a preset value",
|
||||
command: []string{"SETRANGE", "SetRangeKey6", strconv.Itoa(len("This is a preset value") - 1), " replaced"},
|
||||
expectedValue: "This is a preset valu replaced",
|
||||
expectedResponse: len("This is a preset valu replaced"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: " Offset not integer",
|
||||
preset: false,
|
||||
command: []string{"SETRANGE", "key", "offset", "value"},
|
||||
expectedResponse: 0,
|
||||
expectedError: errors.New("offset must be an integer"),
|
||||
},
|
||||
{
|
||||
name: "SETRANGE target is not a string",
|
||||
preset: true,
|
||||
key: "test-int",
|
||||
presetValue: "10",
|
||||
command: []string{"SETRANGE", "test-int", "10", "value"},
|
||||
expectedResponse: 0,
|
||||
expectedError: errors.New("value at key test-int is not a string"),
|
||||
},
|
||||
{
|
||||
name: "Command too short",
|
||||
preset: false,
|
||||
command: []string{"SETRANGE", "key"},
|
||||
expectedResponse: 0,
|
||||
expectedError: errors.New(constants.WrongArgsResponse),
|
||||
},
|
||||
{
|
||||
name: "Command too long",
|
||||
preset: false,
|
||||
command: []string{"SETRANGE", "key", "offset", "value", "value1"},
|
||||
expectedResponse: 0,
|
||||
expectedError: errors.New(constants.WrongArgsResponse),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SETRANGE, %d", i))
|
||||
|
||||
// If there's a preset step, carry it out here
|
||||
if test.preset {
|
||||
if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := mockServer.SetValue(ctx, test.key, internal.AdaptType(test.presetValue)); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
mockServer.KeyUnlock(ctx, test.key)
|
||||
}
|
||||
|
||||
handler := getHandler(test.command[0])
|
||||
if handler == nil {
|
||||
t.Errorf("no handler found for command %s", test.command[0])
|
||||
return
|
||||
}
|
||||
|
||||
res, err := handler(getHandlerFuncParams(ctx, test.command, nil))
|
||||
|
||||
if test.expectedError != nil {
|
||||
if err.Error() != test.expectedError.Error() {
|
||||
t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
rd := resp.NewReader(bytes.NewBuffer(res))
|
||||
rv, _, err := rd.ReadValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if rv.Integer() != test.expectedResponse {
|
||||
t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, rv.Integer())
|
||||
}
|
||||
|
||||
// Get the value from the echovault and check against the expected value
|
||||
if _, err = mockServer.KeyRLock(ctx, test.key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
value, ok := mockServer.GetValue(ctx, test.key).(string)
|
||||
if !ok {
|
||||
t.Error("expected string data type, got another type")
|
||||
}
|
||||
if value != test.expectedValue {
|
||||
t.Errorf("expected value \"%s\", got \"%s\"", test.expectedValue, value)
|
||||
}
|
||||
mockServer.KeyRUnlock(ctx, test.key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_HandleStrLen(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
preset bool
|
||||
key string
|
||||
presetValue string
|
||||
command []string
|
||||
expectedResponse int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Return the correct string length for an existing string",
|
||||
preset: true,
|
||||
key: "StrLenKey1",
|
||||
presetValue: "Test String",
|
||||
command: []string{"STRLEN", "StrLenKey1"},
|
||||
expectedResponse: len("Test String"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "If the string does not exist, return 0",
|
||||
preset: false,
|
||||
key: "StrLenKey2",
|
||||
presetValue: "",
|
||||
command: []string{"STRLEN", "StrLenKey2"},
|
||||
expectedResponse: 0,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Too few args",
|
||||
preset: false,
|
||||
key: "StrLenKey3",
|
||||
presetValue: "",
|
||||
command: []string{"STRLEN"},
|
||||
expectedResponse: 0,
|
||||
expectedError: errors.New(constants.WrongArgsResponse),
|
||||
},
|
||||
{
|
||||
name: "Too many args",
|
||||
preset: false,
|
||||
key: "StrLenKey4",
|
||||
presetValue: "",
|
||||
command: []string{"STRLEN", "StrLenKey4", "StrLenKey5"},
|
||||
expectedResponse: 0,
|
||||
expectedError: errors.New(constants.WrongArgsResponse),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("STRLEN, %d", i))
|
||||
|
||||
if test.preset {
|
||||
_, err := mockServer.CreateKeyAndLock(ctx, test.key)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
mockServer.KeyUnlock(ctx, test.key)
|
||||
}
|
||||
|
||||
handler := getHandler(test.command[0])
|
||||
if handler == nil {
|
||||
t.Errorf("no handler found for command %s", test.command[0])
|
||||
return
|
||||
}
|
||||
|
||||
res, err := handler(getHandlerFuncParams(ctx, test.command, nil))
|
||||
|
||||
if test.expectedError != nil {
|
||||
if err.Error() != test.expectedError.Error() {
|
||||
t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
rd := resp.NewReader(bytes.NewBuffer(res))
|
||||
rv, _, err := rd.ReadValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if rv.Integer() != test.expectedResponse {
|
||||
t.Errorf("expected respons \"%d\", got \"%d\"", test.expectedResponse, rv.Integer())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_HandleSubStr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
preset bool
|
||||
key string
|
||||
presetValue string
|
||||
command []string
|
||||
expectedResponse string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Return substring within the range of the string",
|
||||
preset: true,
|
||||
key: "SubStrKey1",
|
||||
presetValue: "Test String One",
|
||||
command: []string{"SUBSTR", "SubStrKey1", "5", "10"},
|
||||
expectedResponse: "String",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Return substring at the end of the string with exact end index",
|
||||
preset: true,
|
||||
key: "SubStrKey2",
|
||||
presetValue: "Test String Two",
|
||||
command: []string{"SUBSTR", "SubStrKey2", "12", "14"},
|
||||
expectedResponse: "Two",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Return substring at the end of the string with end index greater than length",
|
||||
preset: true,
|
||||
key: "SubStrKey3",
|
||||
presetValue: "Test String Three",
|
||||
command: []string{"SUBSTR", "SubStrKey3", "12", "75"},
|
||||
expectedResponse: "Three",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Return the substring at the start of the string with 0 start index",
|
||||
preset: true,
|
||||
key: "SubStrKey4",
|
||||
presetValue: "Test String Four",
|
||||
command: []string{"SUBSTR", "SubStrKey4", "0", "3"},
|
||||
expectedResponse: "Test",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
// Return the substring with negative start index.
|
||||
// Substring should begin abs(start) from the end of the string when start is negative.
|
||||
name: "Return the substring with negative start index",
|
||||
preset: true,
|
||||
key: "SubStrKey5",
|
||||
presetValue: "Test String Five",
|
||||
command: []string{"SUBSTR", "SubStrKey5", "-11", "10"},
|
||||
expectedResponse: "String",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
// Return reverse substring with end index smaller than start index.
|
||||
// When end index is smaller than start index, the 2 indices are reversed.
|
||||
name: "Return reverse substring with end index smaller than start index",
|
||||
preset: true,
|
||||
key: "SubStrKey6",
|
||||
presetValue: "Test String Six",
|
||||
command: []string{"SUBSTR", "SubStrKey6", "4", "0"},
|
||||
expectedResponse: "tseT",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Command too short",
|
||||
command: []string{"SUBSTR", "key", "10"},
|
||||
expectedError: errors.New(constants.WrongArgsResponse),
|
||||
},
|
||||
{
|
||||
name: "Command too long",
|
||||
command: []string{"SUBSTR", "key", "10", "15", "20"},
|
||||
expectedError: errors.New(constants.WrongArgsResponse),
|
||||
},
|
||||
{
|
||||
name: "Start index is not an integer",
|
||||
command: []string{"SUBSTR", "key", "start", "10"},
|
||||
expectedError: errors.New("start and end indices must be integers"),
|
||||
},
|
||||
{
|
||||
name: "End index is not an integer",
|
||||
command: []string{"SUBSTR", "key", "0", "end"},
|
||||
expectedError: errors.New("start and end indices must be integers"),
|
||||
},
|
||||
{
|
||||
name: "Non-existent key",
|
||||
command: []string{"SUBSTR", "non-existent-key", "0", "10"},
|
||||
expectedError: errors.New("key non-existent-key does not exist"),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SUBSTR, %d", i))
|
||||
|
||||
if test.preset {
|
||||
if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
mockServer.KeyUnlock(ctx, test.key)
|
||||
}
|
||||
|
||||
handler := getHandler(test.command[0])
|
||||
if handler == nil {
|
||||
t.Errorf("no handler found for command %s", test.command[0])
|
||||
return
|
||||
}
|
||||
|
||||
res, err := handler(getHandlerFuncParams(ctx, test.command, nil))
|
||||
|
||||
if test.expectedError != nil {
|
||||
if err.Error() != test.expectedError.Error() {
|
||||
t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
rd := resp.NewReader(bytes.NewBuffer(res))
|
||||
rv, _, err := rd.ReadValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if rv.String() != test.expectedResponse {
|
||||
t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, rv.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user