Files
SugarDB/src/modules/string/commands_test.go

367 lines
12 KiB
Go

// 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
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/echovault/echovault/src/echovault"
"github.com/echovault/echovault/src/utils"
"github.com/tidwall/resp"
"strconv"
"testing"
)
var mockServer *echovault.EchoVault
func init() {
mockServer = echovault.NewEchoVault(
echovault.WithConfig(utils.Config{
DataDir: "",
EvictionPolicy: utils.NoEviction,
}),
)
}
func Test_HandleSetRange(t *testing.T) {
tests := []struct {
preset bool
key string
presetValue string
command []string
expectedValue string
expectedResponse int
expectedError error
}{
{ // 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,
},
{ // 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,
},
{ // 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,
},
{ // 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,
},
{ // 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,
},
{ // 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,
},
{ // Offset not integer
preset: false,
command: []string{"SETRANGE", "key", "offset", "value"},
expectedResponse: 0,
expectedError: errors.New("offset must be an integer"),
},
{ // 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"),
},
{ // Command too short
preset: false,
command: []string{"SETRANGE", "key"},
expectedResponse: 0,
expectedError: errors.New(utils.WrongArgsResponse),
},
{ // Command too long
preset: false,
command: []string{"SETRANGE", "key", "offset", "value", "value1"},
expectedResponse: 0,
expectedError: errors.New(utils.WrongArgsResponse),
},
}
for i, test := range tests {
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, utils.AdaptType(test.presetValue)); err != nil {
t.Error(err)
}
mockServer.KeyUnlock(ctx, test.key)
}
res, err := handleSetRange(ctx, test.command, mockServer, nil)
if test.expectedError != nil {
if err.Error() != test.expectedError.Error() {
t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error())
}
continue
}
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 {
preset bool
key string
presetValue string
command []string
expectedResponse int
expectedError error
}{
{ // 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,
},
{ // If the string does not exist, return 0
preset: false,
key: "StrLenKey2",
presetValue: "",
command: []string{"STRLEN", "StrLenKey2"},
expectedResponse: 0,
expectedError: nil,
},
{ // Too few args
preset: false,
key: "StrLenKey3",
presetValue: "",
command: []string{"STRLEN"},
expectedResponse: 0,
expectedError: errors.New(utils.WrongArgsResponse),
},
{ // Too many args
preset: false,
key: "StrLenKey4",
presetValue: "",
command: []string{"STRLEN", "StrLenKey4", "StrLenKey5"},
expectedResponse: 0,
expectedError: errors.New(utils.WrongArgsResponse),
},
}
for i, test := range tests {
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)
}
res, err := handleStrLen(ctx, test.command, mockServer, nil)
if test.expectedError != nil {
if err.Error() != test.expectedError.Error() {
t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error())
}
continue
}
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 {
preset bool
key string
presetValue string
command []string
expectedResponse string
expectedError error
}{
{ // 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,
},
{ // 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,
},
{ // 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,
},
{ // 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.
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.
preset: true,
key: "SubStrKey6",
presetValue: "Test String Six",
command: []string{"SUBSTR", "SubStrKey6", "4", "0"},
expectedResponse: "tseT",
expectedError: nil,
},
{ // Command too short
command: []string{"SUBSTR", "key", "10"},
expectedError: errors.New(utils.WrongArgsResponse),
},
{
// Command too long
command: []string{"SUBSTR", "key", "10", "15", "20"},
expectedError: errors.New(utils.WrongArgsResponse),
},
{ // Start index is not an integer
command: []string{"SUBSTR", "key", "start", "10"},
expectedError: errors.New("start and end indices must be integers"),
},
{ // End index is not an integer
command: []string{"SUBSTR", "key", "0", "end"},
expectedError: errors.New("start and end indices must be integers"),
},
{ // 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 {
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)
}
res, err := handleSubStr(ctx, test.command, mockServer, nil)
if test.expectedError != nil {
if err.Error() != test.expectedError.Error() {
t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error())
}
continue
}
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())
}
}
}