diff --git a/integration_tests/commands/tests/aggregator.go b/integration_tests/commands/tests/aggregator.go new file mode 100644 index 000000000..9d1442742 --- /dev/null +++ b/integration_tests/commands/tests/aggregator.go @@ -0,0 +1,89 @@ +package tests + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var allTests []Meta + +// SetupCmd struct is used to define a setup command +// Input: Input commands to be executed +// Output: Expected output of the setup command +// Keep the setup tests simple which return "OK" or "integer" or "(nil)" +// For complex setup tests, use the test cases +type SetupCmd struct { + Input []string + Output []interface{} +} + +const ( + EQUAL = "EQUAL" + JSON = "JSON" + ARRAY = "ARRAY" +) + +// Meta struct is used to define a test case +// Name: Name of the test case +// Cmd: Command to be executed +// Setup: Setup commands to be executed +// Input: Input commands to be executed +// Output: Expected output of the test case +// Delays: Delays to be introduced between commands +// CleanupKeys: list of keys to be cleaned up after the test case +type Meta struct { + Name string + Cmd string + Setup []SetupCmd + Input []string + Output []interface{} + Assert []string + Delays []time.Duration + Cleanup []string +} + +// RegisterTests appends a Meta slice to the global test list +func RegisterTests(tests []Meta) { + allTests = append(allTests, tests...) +} + +// GetAllTests returns all registered test cases +func GetAllTests() []Meta { + return allTests +} + +func SwitchAsserts(t *testing.T, kind string, expected, actual interface{}) { + switch kind { + case EQUAL: + assert.Equal(t, expected, actual) + case JSON: + assert.JSONEq(t, expected.(string), actual.(string)) + case ARRAY: + assert.ElementsMatch(t, expected, actual) + } +} + +func Validate(test *Meta) bool { + // Validate test structure + if len(test.Input) != len(test.Output) { + fmt.Printf("Test %s: mismatch between number of inputs (%d) and outputs (%d)", test.Name, len(test.Input), len(test.Output)) + return false + } + if len(test.Delays) > 0 && len(test.Delays) != len(test.Input) { + fmt.Printf("Test %s: mismatch between number of inputs (%d) and delays (%d)", test.Name, len(test.Input), len(test.Delays)) + return false + } + if len(test.Setup) > 0 { + for _, setup := range test.Setup { + if len(setup.Input) != len(setup.Output) { + fmt.Printf("Test %s (Setup): mismatch between number of setup inputs (%d) and outputs (%d)", test.Name, len(setup.Input), len(setup.Output)) + return false + } + } + } + + return true +} diff --git a/integration_tests/commands/tests/get.go b/integration_tests/commands/tests/get.go new file mode 100644 index 000000000..bc0f94f33 --- /dev/null +++ b/integration_tests/commands/tests/get.go @@ -0,0 +1,30 @@ +package tests + +import ( + "time" +) + +var getTestCases = []Meta{ + { + Name: "Get on non-existing key", + Input: []string{"GET k"}, + Output: []interface{}{"(nil)"}, + }, + { + Name: "Get on existing key", + Input: []string{"SET k v", "GET k"}, + Output: []interface{}{"OK", "v"}, + Cleanup: []string{"k"}, + }, + { + Name: "Get with expiration", + Input: []string{"SET k v EX 2", "GET k", "GET k"}, + Output: []interface{}{"OK", "v", "(nil)"}, + Delays: []time.Duration{0, 0, 3 * time.Second}, + Cleanup: []string{"k"}, + }, +} + +func init() { + RegisterTests(getTestCases) +} diff --git a/integration_tests/commands/tests/http_test.go b/integration_tests/commands/tests/http_test.go new file mode 100644 index 000000000..f21bb5758 --- /dev/null +++ b/integration_tests/commands/tests/http_test.go @@ -0,0 +1,63 @@ +package tests + +import ( + "log" + "testing" + "time" + + "github.com/dicedb/dice/config" + "github.com/dicedb/dice/integration_tests/commands/tests/parsers" + "github.com/dicedb/dice/integration_tests/commands/tests/servers" + "github.com/stretchr/testify/assert" +) + +func init() { + parser := config.NewConfigParser() + if err := parser.ParseDefaults(config.DiceConfig); err != nil { + log.Fatalf("failed to load configuration: %v", err) + } +} + +func TestHttpCommands(t *testing.T) { + exec := servers.NewHTTPCommandExecutor() + allTests := GetAllTests() + + for _, test := range allTests { + t.Run(test.Name, func(t *testing.T) { + if !Validate(&test) { + t.Fatal("Test progression failed...") + } + + // Setup commands + if len(test.Setup) > 0 { + for _, setup := range test.Setup { + for idx, cmd := range setup.Input { + output, _ := parsers.HttpCommandExecuter(exec, cmd) + assert.Equal(t, setup.Output[idx], output) + } + } + } + + for idx, cmd := range test.Input { + if len(test.Delays) > 0 { + time.Sleep(test.Delays[idx]) + } + output, _ := parsers.HttpCommandExecuter(exec, cmd) + if len(test.Assert) > 0 { + SwitchAsserts(t, test.Assert[idx], test.Output[idx], output) + } else { + assert.Equal(t, test.Output[idx], output) + } + } + if len(test.Cleanup) > 0 { + // join all the keys to be cleaned up + keys := "" + for _, key := range test.Cleanup { + keys += key + " " + } + parsers.HttpCommandExecuter(exec, `DEL `+keys) + } + }) + + } +} diff --git a/integration_tests/commands/tests/main_test.go b/integration_tests/commands/tests/main_test.go new file mode 100644 index 000000000..766cba7c7 --- /dev/null +++ b/integration_tests/commands/tests/main_test.go @@ -0,0 +1,59 @@ +package tests + +import ( + "context" + "os" + "sync" + "testing" + "time" + + "github.com/dicedb/dice/integration_tests/commands/tests/servers" +) + +func TestMain(m *testing.M) { + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + respOpts := servers.TestServerOptions{ + Port: 9738, + } + httpOpts := servers.TestServerOptions{ + Port: 8083, + } + wsOpts := servers.TestServerOptions{ + Port: 8380, + } + + wg.Add(1) + go func() { + defer wg.Done() + servers.RunRespServer(ctx, &wg, respOpts) + }() + + wg.Add(1) + go func() { + defer wg.Done() + servers.RunHTTPServer(ctx, &wg, httpOpts) + }() + + //TODO: RunWebSocketServer + wg.Add(1) + go func() { + defer wg.Done() + servers.RunWebsocketServer(ctx, &wg, wsOpts) + }() + + // Wait for the server to start + time.Sleep(2 * time.Second) + + // Run the test suite + exitCode := m.Run() + + // Signal all servers to stop + cancel() + + // Wait for all goroutines to finish + wg.Wait() + + // Exit with the appropriate code + os.Exit(exitCode) +} diff --git a/integration_tests/commands/tests/parsers/common.go b/integration_tests/commands/tests/parsers/common.go new file mode 100644 index 000000000..d7efe262d --- /dev/null +++ b/integration_tests/commands/tests/parsers/common.go @@ -0,0 +1,12 @@ +package parsers + +func ParseResponse(response interface{}) interface{} { + switch response := response.(type) { + case float64: + return int64(response) + case nil: + return "(nil)" + default: + return response + } +} diff --git a/integration_tests/commands/tests/parsers/http.go b/integration_tests/commands/tests/parsers/http.go new file mode 100644 index 000000000..49bb0f94c --- /dev/null +++ b/integration_tests/commands/tests/parsers/http.go @@ -0,0 +1,34 @@ +package parsers + +import ( + "strings" + + "github.com/dicedb/dice/integration_tests/commands/tests/servers" +) + +func HTTPCommandExecuter(exec *servers.HTTPCommandExecutor, cmd string) (interface{}, error) { + // convert the command to a HTTPCommand + // cmd starts with Command and Body is values after that + tokens := strings.Split(cmd, " ") + command := tokens[0] + body := make(map[string]interface{}) + if len(tokens) > 1 { + // convert the tokens []string to []interface{} + values := make([]interface{}, len(tokens[1:])) + for i, v := range tokens[1:] { + values[i] = v + } + body["values"] = values + } else { + body["values"] = []interface{}{} + } + diceHTTPCmd := servers.HTTPCommand{ + Command: strings.ToLower(command), + Body: body, + } + res, err := exec.FireCommand(diceHTTPCmd) + if err != nil { + return nil, err + } + return ParseResponse(res), nil +} diff --git a/integration_tests/commands/tests/parsers/resp.go b/integration_tests/commands/tests/parsers/resp.go new file mode 100644 index 000000000..e70dbb6a5 --- /dev/null +++ b/integration_tests/commands/tests/parsers/resp.go @@ -0,0 +1,41 @@ +package parsers + +import ( + "io" + "log/slog" + "net" + "os" + + "github.com/dicedb/dice/internal/clientio" + "github.com/dicedb/dice/testutils" +) + +func RespCommandExecuter(conn net.Conn, cmd string) interface{} { + var err error + args := testutils.ParseCommand(cmd) + _, err = conn.Write(clientio.Encode(args, false)) + if err != nil { + slog.Error( + "error while firing command", + slog.Any("error", err), + slog.String("command", cmd), + ) + os.Exit(1) + } + + rp := clientio.NewRESPParser(conn) + v, err := rp.DecodeOne() + if err != nil { + if err == io.EOF { + return nil + } + slog.Error( + "error while firing command", + slog.Any("error", err), + slog.String("command", cmd), + ) + os.Exit(1) + } + + return v +} diff --git a/integration_tests/commands/tests/parsers/websocket.go b/integration_tests/commands/tests/parsers/websocket.go new file mode 100644 index 000000000..c6a88e9db --- /dev/null +++ b/integration_tests/commands/tests/parsers/websocket.go @@ -0,0 +1,39 @@ +package parsers + +import ( + "encoding/json" + "fmt" + + "github.com/gorilla/websocket" +) + +func FireWSCommandAndReadResponse(conn *websocket.Conn, cmd string) (interface{}, error) { + err := FireWSCommand(conn, cmd) + if err != nil { + return nil, err + } + + // read the response + _, resp, err := conn.ReadMessage() + if err != nil { + return nil, err + } + + // marshal to json + var respJSON interface{} + if err = json.Unmarshal(resp, &respJSON); err != nil { + return nil, fmt.Errorf("error unmarshaling response") + } + respJSON = ParseResponse(respJSON) + return respJSON, nil +} + +func FireWSCommand(conn *websocket.Conn, cmd string) error { + // send request + err := conn.WriteMessage(websocket.TextMessage, []byte(cmd)) + if err != nil { + return err + } + + return nil +} diff --git a/integration_tests/commands/tests/resp_test.go b/integration_tests/commands/tests/resp_test.go new file mode 100644 index 000000000..d38ea2bd3 --- /dev/null +++ b/integration_tests/commands/tests/resp_test.go @@ -0,0 +1,60 @@ +package tests + +import ( + "log" + "testing" + "time" + + "github.com/dicedb/dice/config" + "github.com/dicedb/dice/integration_tests/commands/tests/parsers" + "github.com/dicedb/dice/integration_tests/commands/tests/servers" + "github.com/stretchr/testify/assert" +) + +func init() { + parser := config.NewConfigParser() + if err := parser.ParseDefaults(config.DiceConfig); err != nil { + log.Fatalf("failed to load configuration: %v", err) + } +} + +func TestRespCommands(t *testing.T) { + conn := servers.GetRespConn() + allTests := GetAllTests() + + for _, test := range allTests { + t.Run(test.Name, func(t *testing.T) { + if !Validate(&test) { + t.Fatal("Test progression failed...") + } + + if len(test.Setup) > 0 { + for _, setup := range test.Setup { + for idx, cmd := range setup.Input { + output := parsers.RespCommandExecuter(conn, cmd) + assert.Equal(t, setup.Output[idx], output) + } + } + } + + for idx, cmd := range test.Input { + if len(test.Delays) > 0 { + time.Sleep(test.Delays[idx]) + } + + output := parsers.RespCommandExecuter(conn, cmd) + assert.Equal(t, test.Output[idx], output) + } + + if len(test.Cleanup) > 0 { + // join all the keys to be cleaned up + keys := "" + for _, key := range test.Cleanup { + keys += key + " " + } + parsers.RespCommandExecuter(conn, `DEL `+keys) + } + }) + + } +} diff --git a/integration_tests/commands/tests/servers/common.go b/integration_tests/commands/tests/servers/common.go new file mode 100644 index 000000000..3f05d84f6 --- /dev/null +++ b/integration_tests/commands/tests/servers/common.go @@ -0,0 +1,6 @@ +package servers + +type TestServerOptions struct { + Port int + MaxClients int32 +} diff --git a/integration_tests/commands/tests/servers/http.go b/integration_tests/commands/tests/servers/http.go new file mode 100644 index 000000000..724c35986 --- /dev/null +++ b/integration_tests/commands/tests/servers/http.go @@ -0,0 +1,138 @@ +package servers + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "strings" + "sync" + "time" + + "github.com/dicedb/dice/internal/server/utils" + + "github.com/dicedb/dice/config" + derrors "github.com/dicedb/dice/internal/errors" + "github.com/dicedb/dice/internal/querymanager" + "github.com/dicedb/dice/internal/server" + "github.com/dicedb/dice/internal/shard" + dstore "github.com/dicedb/dice/internal/store" +) + +type HTTPCommandExecutor struct { + httpClient *http.Client + baseURL string +} + +func NewHTTPCommandExecutor() *HTTPCommandExecutor { + return &HTTPCommandExecutor{ + baseURL: "http://localhost:8083", + httpClient: &http.Client{ + Timeout: time.Second * 100, + }, + } +} + +type HTTPCommand struct { + Command string + Body map[string]interface{} +} + +func (cmd HTTPCommand) IsEmptyCommand() bool { + return cmd.Command == "" +} + +func (e *HTTPCommandExecutor) FireCommand(cmd HTTPCommand) (interface{}, error) { + command := strings.ToUpper(cmd.Command) + var body []byte + if cmd.Body != nil { + var err error + body, err = json.Marshal(cmd.Body) + // Handle error during JSON marshaling + if err != nil { + return nil, err + } + } + + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "POST", e.baseURL+"/"+command, bytes.NewBuffer(body)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := e.httpClient.Do(req) + + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if cmd.Command != "Q.WATCH" { + var result utils.HTTPResponse + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, err + } + + return result.Data, nil + } + var result interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (e *HTTPCommandExecutor) Name() string { + return "HTTP" +} + +func RunHTTPServer(ctx context.Context, wg *sync.WaitGroup, opt TestServerOptions) { + config.DiceConfig.Network.IOBufferLength = 16 + config.DiceConfig.Persistence.WriteAOFOnCleanup = false + + globalErrChannel := make(chan error) + watchChan := make(chan dstore.QueryWatchEvent, config.DiceConfig.Performance.WatchChanBufSize) + shardManager := shard.NewShardManager(1, watchChan, nil, globalErrChannel) + queryWatcherLocal := querymanager.NewQueryManager() + config.DiceConfig.HTTP.Port = opt.Port + // Initialize the HTTPServer + testServer := server.NewHTTPServer(shardManager, nil) + // Inform the user that the server is starting + fmt.Println("Starting the test HTTP server on the port", config.DiceConfig.HTTP.Port) + shardManagerCtx, cancelShardManager := context.WithCancel(ctx) + wg.Add(1) + go func() { + defer wg.Done() + shardManager.Run(shardManagerCtx) + }() + + wg.Add(1) + go func() { + defer wg.Done() + queryWatcherLocal.Run(ctx, watchChan) + }() + + // Start the server in a goroutine + wg.Add(1) + go func() { + defer wg.Done() + err := testServer.Run(ctx) + if err != nil { + cancelShardManager() + if errors.Is(err, derrors.ErrAborted) { + return + } + if err.Error() != "http: Server closed" { + log.Fatalf("Http test server encountered an error: %v", err) + } + log.Printf("Http test server encountered an error: %v", err) + } + }() +} diff --git a/integration_tests/commands/tests/servers/resp.go b/integration_tests/commands/tests/servers/resp.go new file mode 100644 index 000000000..ceb40e397 --- /dev/null +++ b/integration_tests/commands/tests/servers/resp.go @@ -0,0 +1,84 @@ +package servers + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "os" + "sync" + + "github.com/dicedb/dice/config" + derrors "github.com/dicedb/dice/internal/errors" + "github.com/dicedb/dice/internal/iothread" + "github.com/dicedb/dice/internal/server/resp" + "github.com/dicedb/dice/internal/shard" + dstore "github.com/dicedb/dice/internal/store" + "github.com/dicedb/dice/internal/wal" + "github.com/dicedb/dice/internal/watchmanager" +) + +func GetRespConn() net.Conn { + conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", config.DiceConfig.RespServer.Port)) + if err != nil { + panic(err) + } + return conn +} + +func RunRespServer(ctx context.Context, wg *sync.WaitGroup, opt TestServerOptions) { + config.DiceConfig.Network.IOBufferLength = 16 + config.DiceConfig.Persistence.WriteAOFOnCleanup = false + + // #1261: Added here to prevent resp integration tests from failing on lower-spec machines + config.DiceConfig.Memory.KeysLimit = 2000 + if opt.Port != 0 { + config.DiceConfig.RespServer.Port = opt.Port + } else { + config.DiceConfig.RespServer.Port = 9739 + } + + queryWatchChan := make(chan dstore.QueryWatchEvent, config.DiceConfig.Performance.WatchChanBufSize) + cmdWatchChan := make(chan dstore.CmdWatchEvent, config.DiceConfig.Performance.WatchChanBufSize) + cmdWatchSubscriptionChan := make(chan watchmanager.WatchSubscription) + gec := make(chan error) + shardManager := shard.NewShardManager(1, queryWatchChan, cmdWatchChan, gec) + ioThreadManager := iothread.NewManager(20000, shardManager) + // Initialize the RESP Server + wl, _ := wal.NewNullWAL() + testServer := resp.NewServer(shardManager, ioThreadManager, cmdWatchSubscriptionChan, cmdWatchChan, gec, wl) + + fmt.Println("Starting the test RESP server on the port", config.DiceConfig.RespServer.Port) + + shardManagerCtx, cancelShardManager := context.WithCancel(ctx) + wg.Add(1) + go func() { + defer wg.Done() + shardManager.Run(shardManagerCtx) + }() + + // Start the server in a goroutine + wg.Add(1) + go func() { + defer wg.Done() + if err := testServer.Run(ctx); err != nil { + if errors.Is(err, derrors.ErrAborted) { + cancelShardManager() + return + } + slog.Error("Test server encountered an error", slog.Any("error", err)) + os.Exit(1) + } + }() + + go func() { + for err := range gec { + if err != nil && errors.Is(err, derrors.ErrAborted) { + // if either the AsyncServer/RESPServer or the HTTPServer received an abort command, + // cancel the context, helping gracefully exiting all servers + _ = ctx.Err() + } + } + }() +} diff --git a/integration_tests/commands/tests/servers/websocket.go b/integration_tests/commands/tests/servers/websocket.go new file mode 100644 index 000000000..c73d8c6ac --- /dev/null +++ b/integration_tests/commands/tests/servers/websocket.go @@ -0,0 +1,106 @@ +package servers + +import ( + "context" + "errors" + "log/slog" + "net/http" + "sync" + "time" + + "github.com/dicedb/dice/config" + derrors "github.com/dicedb/dice/internal/errors" + "github.com/dicedb/dice/internal/querymanager" + "github.com/dicedb/dice/internal/server" + "github.com/dicedb/dice/internal/shard" + dstore "github.com/dicedb/dice/internal/store" + "github.com/gorilla/websocket" +) + +const ( + URL = "ws://localhost:8380" + testPort1 = 8380 + testPort2 = 8381 +) + +type WebsocketCommandExecutor struct { + baseURL string + websocketClient *http.Client + upgrader websocket.Upgrader +} + +func NewWebsocketCommandExecutor() *WebsocketCommandExecutor { + return &WebsocketCommandExecutor{ + baseURL: URL, + websocketClient: &http.Client{ + Timeout: time.Second * 100, + }, + upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + }, + } +} + +func (e *WebsocketCommandExecutor) ConnectToServer() *websocket.Conn { + // connect with Websocket Server + conn, resp, err := websocket.DefaultDialer.Dial(URL, nil) + if err != nil { + return nil + } + if resp != nil { + resp.Body.Close() + } + return conn +} + +func (e *WebsocketCommandExecutor) FireCommand(conn *websocket.Conn, cmd string) error { + // send request + err := conn.WriteMessage(websocket.TextMessage, []byte(cmd)) + if err != nil { + return err + } + + return nil +} + +func RunWebsocketServer(ctx context.Context, wg *sync.WaitGroup, opt TestServerOptions) { + config.DiceConfig.Network.IOBufferLength = 16 + config.DiceConfig.Persistence.WriteAOFOnCleanup = false + + // Initialize WebsocketServer + globalErrChannel := make(chan error) + watchChan := make(chan dstore.QueryWatchEvent, config.DiceConfig.Performance.WatchChanBufSize) + shardManager := shard.NewShardManager(1, watchChan, nil, globalErrChannel) + queryWatcherLocal := querymanager.NewQueryManager() + config.DiceConfig.WebSocket.Port = opt.Port + testServer := server.NewWebSocketServer(shardManager, testPort1, nil) + shardManagerCtx, cancelShardManager := context.WithCancel(ctx) + + // run shard manager + wg.Add(1) + go func() { + defer wg.Done() + shardManager.Run(shardManagerCtx) + }() + + // run query manager + wg.Add(1) + go func() { + defer wg.Done() + queryWatcherLocal.Run(ctx, watchChan) + }() + + // start websocket server + wg.Add(1) + go func() { + defer wg.Done() + srverr := testServer.Run(ctx) + if srverr != nil { + cancelShardManager() + if errors.Is(srverr, derrors.ErrAborted) { + return + } + slog.Debug("Websocket test server encountered an error: %v", slog.Any("error", srverr)) + } + }() +} diff --git a/integration_tests/commands/tests/set.go b/integration_tests/commands/tests/set.go new file mode 100644 index 000000000..70f93c2c9 --- /dev/null +++ b/integration_tests/commands/tests/set.go @@ -0,0 +1,129 @@ +package tests + +import ( + "strconv" + "time" +) + +var expiryTime = strconv.FormatInt(time.Now().Add(1*time.Minute).UnixMilli(), 10) + +var setTestCases = []Meta{ + { + Name: "Set and Get Simple Value", + Input: []string{"SET k v", "GET k"}, + Output: []interface{}{"OK", "v"}, + Cleanup: []string{"k"}, + }, + { + Name: "Set and Get Integer Value", + Input: []string{"SET k 123456789", "GET k"}, + Output: []interface{}{"OK", int64(123456789)}, + Cleanup: []string{"k"}, + }, + { + Name: "Overwrite Existing Key", + Input: []string{"SET k v1", "SET k 5", "GET k"}, + Output: []interface{}{"OK", "OK", int64(5)}, + Cleanup: []string{"k"}, + }, + { + Name: "Set with EX option", + Input: []string{"SET k v EX 2", "GET k", "SLEEP 3", "GET k"}, + Output: []interface{}{"OK", "v", "OK", "(nil)"}, + Cleanup: []string{"k"}, + }, + { + Name: "Set with PX option", + Input: []string{"SET k v PX 2000", "GET k", "SLEEP 3", "GET k"}, + Output: []interface{}{"OK", "v", "OK", "(nil)"}, + Cleanup: []string{"k"}, + }, + { + Name: "Set with EX and PX option", + Input: []string{"SET k v EX 2 PX 2000"}, + Output: []interface{}{"ERR syntax error"}, + Cleanup: []string{"k"}, + }, + { + Name: "XX on non-existing key", + Input: []string{"DEL k", "SET k v XX", "GET k"}, + Output: []interface{}{int64(0), "(nil)", "(nil)"}, + Cleanup: []string{"k"}, + }, + { + Name: "NX on non-existing key", + Input: []string{"SET k v NX", "GET k"}, + Output: []interface{}{"OK", "v"}, + Cleanup: []string{"k"}, + }, + { + Name: "NX on existing key", + Input: []string{"DEL k", "SET k v NX", "GET k", "SET k v NX"}, + Output: []interface{}{int64(0), "OK", "v", "(nil)"}, + Cleanup: []string{"k"}, + }, + { + Name: "PXAT option", + Input: []string{"SET k v PXAT " + expiryTime, "GET k"}, + Output: []interface{}{"OK", "v"}, + Cleanup: []string{"k"}, + }, + { + Name: "PXAT option with delete", + Input: []string{"SET k1 v1 PXAT " + expiryTime, "GET k1", "SLEEP 2", "DEL k1"}, + Output: []interface{}{"OK", "v1", "OK", int64(1)}, + Cleanup: []string{"k1"}, + }, + { + Name: "PXAT option with invalid unix time ms", + Input: []string{"SET k2 v2 PXAT 123123", "GET k2"}, + Output: []interface{}{"OK", "(nil)"}, + Cleanup: []string{"k2"}, + }, + { + Name: "XX on existing key", + Input: []string{"SET k v1", "SET k v2 XX", "GET k"}, + Output: []interface{}{"OK", "OK", "v2"}, + Cleanup: []string{"k"}, + }, + { + Name: "Multiple XX operations", + Input: []string{"SET k v1", "SET k v2 XX", "SET k v3 XX", "GET k"}, + Output: []interface{}{"OK", "OK", "OK", "v3"}, + Cleanup: []string{"k"}, + }, + { + Name: "EX option", + Input: []string{"SET k v EX 1", "GET k", "SLEEP 2", "GET k"}, + Output: []interface{}{"OK", "v", "OK", "(nil)"}, + Cleanup: []string{"k"}, + }, + { + Name: "XX option", + Input: []string{"SET k v XX EX 1", "GET k", "SLEEP 2", "GET k", "SET k v XX EX 1", "GET k"}, + Output: []interface{}{"(nil)", "(nil)", "OK", "(nil)", "(nil)", "(nil)"}, + Cleanup: []string{"k"}, + }, + { + Name: "GET with Existing Value", + Input: []string{"SET k v", "SET k vv GET"}, + Output: []interface{}{"OK", "v"}, + Cleanup: []string{"k"}, + }, + { + Name: "GET with Non-Existing Value", + Input: []string{"SET k vv GET"}, + Output: []interface{}{"(nil)"}, + Cleanup: []string{"k"}, + }, + { + Name: "GET with wrong type of value", + Input: []string{"sadd k v", "SET k vv GET"}, + Output: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, + Cleanup: []string{"k"}, + }, +} + +func init() { + RegisterTests(setTestCases) +} diff --git a/integration_tests/commands/tests/websocket_test.go b/integration_tests/commands/tests/websocket_test.go new file mode 100644 index 000000000..867f173b6 --- /dev/null +++ b/integration_tests/commands/tests/websocket_test.go @@ -0,0 +1,67 @@ +package tests + +import ( + "log" + "testing" + "time" + + "github.com/dicedb/dice/config" + "github.com/dicedb/dice/integration_tests/commands/tests/parsers" + "github.com/dicedb/dice/integration_tests/commands/tests/servers" + "github.com/stretchr/testify/assert" +) + +func init() { + parser := config.NewConfigParser() + if err := parser.ParseDefaults(config.DiceConfig); err != nil { + log.Fatalf("failed to load configuration: %v", err) + } +} + +func TestWebsocketCommands(t *testing.T) { + exec := servers.NewWebsocketCommandExecutor() + allTests := GetAllTests() + + conn := exec.ConnectToServer() + if conn == nil { + t.Fatal("Failed to connect to the server") + } + + for _, test := range allTests { + t.Run(test.Name, func(t *testing.T) { + if !Validate(&test) { + t.Fatal("Test progression failed...") + } + // Setup commands + if len(test.Setup) > 0 { + for _, setup := range test.Setup { + for idx, cmd := range setup.Input { + output, _ := parsers.FireWSCommandAndReadResponse(conn, cmd) + assert.Equal(t, setup.Output[idx], output) + } + } + } + for idx, cmd := range test.Input { + if len(test.Delays) > 0 { + time.Sleep(test.Delays[idx]) + } + output, _ := parsers.FireWSCommandAndReadResponse(conn, cmd) + if len(test.Assert) > 0 { + SwitchAsserts(t, test.Assert[idx], test.Output[idx], output) + } else { + assert.Equal(t, test.Output[idx], output) + } + } + if len(test.Cleanup) > 0 { + // join all the keys to be cleaned up + keys := "" + for _, key := range test.Cleanup { + keys += key + " " + } + parsers.FireWSCommandAndReadResponse(conn, `DEL `+keys) + } + }) + + } + +}