diff --git a/coverage/coverage.out b/coverage/coverage.out index 02738a7..24e1655 100644 --- a/coverage/coverage.out +++ b/coverage/coverage.out @@ -308,6 +308,7 @@ github.com/echovault/echovault/internal/aof/log/store.go:193.2,193.47 1 0 github.com/echovault/echovault/internal/aof/log/store.go:193.47,195.3 1 0 github.com/echovault/echovault/internal/aof/log/store.go:196.2,196.12 1 0 github.com/echovault/echovault/internal/aof/log/store.go:199.41,203.2 3 1 + github.com/echovault/echovault/internal/clock/clock.go:14.23,16.43 1 1 github.com/echovault/echovault/internal/clock/clock.go:16.43,18.3 1 1 github.com/echovault/echovault/internal/clock/clock.go:19.2,19.20 1 0 @@ -538,6 +539,7 @@ github.com/echovault/echovault/internal/aof/engine.go:196.55,198.3 1 0 github.com/echovault/echovault/internal/aof/engine.go:199.2,199.53 1 1 github.com/echovault/echovault/internal/aof/engine.go:199.53,201.3 1 0 github.com/echovault/echovault/internal/aof/engine.go:202.2,202.12 1 1 + github.com/echovault/echovault/internal/clock/clock.go:14.23,16.43 1 1 github.com/echovault/echovault/internal/clock/clock.go:16.43,18.3 1 1 github.com/echovault/echovault/internal/clock/clock.go:19.2,19.20 1 0 @@ -1009,6 +1011,7 @@ github.com/echovault/echovault/internal/config/config.go:254.2,256.45 2 0 github.com/echovault/echovault/internal/config/config.go:256.45,258.3 1 0 github.com/echovault/echovault/internal/config/config.go:260.2,260.18 1 0 github.com/echovault/echovault/internal/config/default.go:9.29,42.2 3 0 + github.com/echovault/echovault/internal/eviction/lfu.go:35.29,42.2 3 1 github.com/echovault/echovault/internal/eviction/lfu.go:44.34,46.2 1 1 github.com/echovault/echovault/internal/eviction/lfu.go:48.44,50.54 1 1 @@ -1043,6 +1046,7 @@ github.com/echovault/echovault/internal/eviction/lru.go:92.73,94.3 1 0 github.com/echovault/echovault/internal/eviction/lru.go:95.2,95.19 1 0 github.com/echovault/echovault/internal/eviction/lru.go:95.19,97.3 1 0 github.com/echovault/echovault/internal/eviction/lru.go:100.50,103.2 2 1 + github.com/echovault/echovault/internal/utils.go:41.38,45.16 2 0 github.com/echovault/echovault/internal/utils.go:45.16,47.3 1 0 github.com/echovault/echovault/internal/utils.go:49.2,49.15 1 0 @@ -1819,6 +1823,7 @@ github.com/echovault/echovault/internal/modules/list/key_funcs.go:115.2,119.8 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:122.75,123.19 1 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:123.19,125.3 1 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:126.2,130.8 1 1 + github.com/echovault/echovault/internal/raft/fsm.go:48.36,52.2 1 0 github.com/echovault/echovault/internal/raft/fsm.go:55.50,56.18 1 0 github.com/echovault/echovault/internal/raft/fsm.go:57.10,57.10 0 0 @@ -4160,6 +4165,7 @@ github.com/echovault/echovault/internal/modules/set/commands.go:159.70,161.16 2 github.com/echovault/echovault/internal/modules/set/commands.go:161.16,163.3 1 0 github.com/echovault/echovault/internal/modules/set/commands.go:165.2,169.37 3 1 github.com/echovault/echovault/internal/modules/set/commands.go:169.37,170.14 1 1 + github.com/echovault/echovault/internal/modules/set/commands.go:170.14,172.4 1 1 github.com/echovault/echovault/internal/modules/set/commands.go:173.3,174.10 2 1 github.com/echovault/echovault/internal/modules/set/commands.go:174.10,177.4 1 1 @@ -4405,6 +4411,7 @@ github.com/echovault/echovault/internal/modules/set/set.go:184.31,185.19 1 1 github.com/echovault/echovault/internal/modules/set/set.go:186.9,187.17 1 1 github.com/echovault/echovault/internal/modules/set/set.go:188.9,191.15 3 1 github.com/echovault/echovault/internal/modules/set/set.go:192.10,195.28 3 1 + github.com/echovault/echovault/internal/raft/fsm.go:48.36,52.2 1 0 github.com/echovault/echovault/internal/raft/fsm.go:55.50,56.18 1 0 github.com/echovault/echovault/internal/raft/fsm.go:57.10,57.10 0 0 @@ -5545,6 +5552,7 @@ github.com/echovault/echovault/internal/modules/sorted_set/utils.go:162.3,162.13 github.com/echovault/echovault/internal/modules/sorted_set/utils.go:163.12,164.16 1 1 github.com/echovault/echovault/internal/modules/sorted_set/utils.go:164.16,166.4 1 1 github.com/echovault/echovault/internal/modules/sorted_set/utils.go:167.3,167.13 1 1 + github.com/echovault/echovault/internal/raft/fsm.go:48.36,52.2 1 0 github.com/echovault/echovault/internal/raft/fsm.go:55.50,56.18 1 0 github.com/echovault/echovault/internal/raft/fsm.go:57.10,57.10 0 0 diff --git a/internal/modules/connection/commands.go b/internal/modules/connection/commands.go index 6682827..f9ed255 100644 --- a/internal/modules/connection/commands.go +++ b/internal/modules/connection/commands.go @@ -154,16 +154,16 @@ func handleSwapDB(params internal.HandlerFuncParams) ([]byte, error) { database1, err := strconv.Atoi(params.Command[1]) if err != nil { - return nil, err + return nil, errors.New("both database indices must be integers") } database2, err := strconv.Atoi(params.Command[2]) if err != nil { - return nil, err + return nil, errors.New("both database indices must be integers") } if database1 < 0 || database2 < 0 { - return nil, errors.New("database must be >= 0") + return nil, errors.New("database indices must be >= 0") } params.SwapDBs(database1, database2) diff --git a/internal/modules/connection/commands_test.go b/internal/modules/connection/commands_test.go index 6b33e9e..4fbf9bb 100644 --- a/internal/modules/connection/commands_test.go +++ b/internal/modules/connection/commands_test.go @@ -714,4 +714,287 @@ func Test_Connection(t *testing.T) { }) } }) + + t.Run("Test_HandleSwapDBs", func(t *testing.T) { + t.Parallel() + + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + mockServer, err := setUpServer(port, false, "") + if err != nil { + t.Error(err) + return + } + go func() { + mockServer.Start() + }() + t.Cleanup(func() { + mockServer.ShutDown() + }) + + tests := []struct { + name string + presetValues map[int]map[string]string + database0 string + database1 string + getCommand []resp.Value + swapCommand []resp.Value + want0 []resp.Value + want1 []resp.Value + wantErr error + }{ + { + name: "1. Successfully swap databases", + presetValues: map[int]map[string]string{ + 0: {"key1": "value-01", "key2": "value-02", "key3": "value-03", "key4": "value-04", "key5": "value-05"}, + 1: {"key1": "value-11", "key2": "value-12", "key3": "value-13", "key4": "value-14", "key5": "value-15"}, + }, + database0: "0", + database1: "1", + getCommand: []resp.Value{ + resp.StringValue("MGET"), + resp.StringValue("key1"), resp.StringValue("key2"), resp.StringValue("key3"), + resp.StringValue("key4"), resp.StringValue("key5"), + }, + swapCommand: []resp.Value{ + resp.StringValue("SWAPDB"), resp.StringValue("0"), resp.StringValue("1"), + }, + want0: []resp.Value{ + resp.StringValue("value-01"), resp.StringValue("value-02"), resp.StringValue("value-03"), + resp.StringValue("value-04"), resp.StringValue("value-05"), + }, + want1: []resp.Value{ + resp.StringValue("value-11"), resp.StringValue("value-12"), resp.StringValue("value-13"), + resp.StringValue("value-14"), resp.StringValue("value-15"), + }, + wantErr: nil, + }, + { + name: "2. First database index is not an integer", + presetValues: nil, + database0: "index0", + database1: "1", + getCommand: make([]resp.Value, 0), + swapCommand: []resp.Value{ + resp.StringValue("SWAPDB"), resp.StringValue("index0"), resp.StringValue("1"), + }, + want0: make([]resp.Value, 0), + want1: make([]resp.Value, 0), + wantErr: errors.New("both database indices must be integers"), + }, + { + name: "3. Second database index is not an integer", + presetValues: nil, + database0: "0", + database1: "index1", + getCommand: make([]resp.Value, 0), + swapCommand: []resp.Value{ + resp.StringValue("SWAPDB"), resp.StringValue("0"), resp.StringValue("index1"), + }, + want0: make([]resp.Value, 0), + want1: make([]resp.Value, 0), + wantErr: errors.New("both database indices must be integers"), + }, + { + name: "4. First database index is < 0", + presetValues: nil, + database0: "-1", + database1: "1", + getCommand: make([]resp.Value, 0), + swapCommand: []resp.Value{ + resp.StringValue("SWAPDB"), resp.StringValue("-1"), resp.StringValue("1"), + }, + want0: make([]resp.Value, 0), + want1: make([]resp.Value, 0), + wantErr: errors.New("database indices must be >= 0"), + }, + { + name: "5. Second database index is < 0", + presetValues: nil, + database0: "1", + database1: "-1", + getCommand: make([]resp.Value, 0), + swapCommand: []resp.Value{ + resp.StringValue("SWAPDB"), resp.StringValue("0"), resp.StringValue("-1"), + }, + want0: make([]resp.Value, 0), + want1: make([]resp.Value, 0), + wantErr: errors.New("database indices must be >= 0"), + }, + { + name: "6. Command too short", + presetValues: nil, + database0: "-1", + database1: "1", + getCommand: make([]resp.Value, 0), + swapCommand: []resp.Value{resp.StringValue("SWAPDB"), resp.StringValue("0")}, + want0: make([]resp.Value, 0), + want1: make([]resp.Value, 0), + wantErr: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Command too long", + presetValues: nil, + database0: "-1", + database1: "1", + getCommand: make([]resp.Value, 0), + swapCommand: []resp.Value{ + resp.StringValue("SWAPDB"), resp.StringValue("0"), + resp.StringValue("1"), resp.StringValue("2"), + }, + want0: make([]resp.Value, 0), + want1: make([]resp.Value, 0), + wantErr: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + // Set values for database 0 and 1. + if test.presetValues != nil { + for db, data := range test.presetValues { + _ = mockServer.SelectDB(db) + if _, err = mockServer.MSet(data); err != nil { + t.Error(err) + return + } + } + } + + // Create TPC connection for database 0 + conn1, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + client1 := resp.NewConn(conn1) + if len(test.getCommand) > 0 { + // Select database 0 for connection 1 + if err = client1.WriteArray([]resp.Value{ + resp.StringValue("SELECT"), + resp.StringValue(test.database0), + }); err != nil { + t.Error(err) + return + } + res, _, err := client1.ReadValue() + if err != nil { + t.Error(err) + return + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expcted OK response when selecting database, got %s", res.String()) + return + } + + // Check that the connection reads values from database 0 + if err = client1.WriteArray(test.getCommand); err != nil { + t.Error(err) + return + } + res, _, err = client1.ReadValue() + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(test.want0, res.Array()) { + t.Errorf("expected response %+v, got %+v", test.want0, res.Array()) + } + } + + // Create TCP connection for database 1 + conn2, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + client2 := resp.NewConn(conn2) + if len(test.getCommand) > 0 { + // Select database 1 for the second connection. + if err = client2.WriteArray([]resp.Value{ + resp.StringValue("SELECT"), + resp.StringValue(test.database1), + }); err != nil { + t.Error(err) + return + } + res, _, err := client2.ReadValue() + if err != nil { + t.Error(err) + return + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expcted OK response when selecting database, got %s", res.String()) + return + } + // Check that the connection reads values from database 1. + if err = client2.WriteArray(test.getCommand); err != nil { + t.Error(err) + return + } + res, _, err = client2.ReadValue() + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(test.want1, res.Array()) { + t.Errorf("expected response %+v, got %+v", test.want1, res.Array()) + } + } + + // Run SWAPDB command + if err = client1.WriteArray(test.swapCommand); err != nil { + t.Error(err) + return + } + res, _, err := client1.ReadValue() + if err != nil { + t.Error(err) + return + } + // If we expect an error check the error. + if test.wantErr != nil { + if !strings.Contains(res.Error().Error(), test.wantErr.Error()) { + t.Errorf("expected error response to contain \"%s\", go \"%s\"", + test.wantErr.Error(), res.Error().Error()) + } + continue + } + // Check if response is OK. + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected OK response from SWAPDB command, got %s", res.String()) + return + } + + // Check that the first connection now reads values from database 1 + if err = client1.WriteArray(test.getCommand); err != nil { + t.Error(err) + return + } + res, _, err = client1.ReadValue() + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(test.want1, res.Array()) { + t.Errorf("expected response %+v, got %+v", test.want1, res.Array()) + } + + // Check that the second connection now reads values from database 0 + if err = client2.WriteArray(test.getCommand); err != nil { + t.Error(err) + return + } + res, _, err = client2.ReadValue() + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(test.want0, res.Array()) { + t.Errorf("expected response %+v, got %+v", test.want0, res.Array()) + } + } + }) }