diff --git a/integration_tests/commands/tests/aggregator.go b/integration_tests/commands/tests/aggregator.go index 838a4988f..347a83c6e 100644 --- a/integration_tests/commands/tests/aggregator.go +++ b/integration_tests/commands/tests/aggregator.go @@ -1,14 +1,45 @@ package tests -import "time" +import ( + "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 } @@ -22,3 +53,14 @@ func RegisterTests(tests []Meta) { func GetAllTests() []Meta { return allTests } + +func SwitchAsserts(t *testing.T, kind string, expected interface{}, 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) + } +} 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..e563614bc --- /dev/null +++ b/integration_tests/commands/tests/http_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" + "gotest.tools/v3/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) { + + // 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 index 7c87fee05..fd83db3cc 100644 --- a/integration_tests/commands/tests/main_test.go +++ b/integration_tests/commands/tests/main_test.go @@ -13,17 +13,24 @@ import ( func TestMain(m *testing.M) { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup - opts := servers.TestServerOptions{ + respOpts := servers.TestServerOptions{ Port: 9738, } wg.Add(1) go func() { defer wg.Done() - servers.RunRespServer(ctx, &wg, opts) + servers.RunRespServer(ctx, &wg, respOpts) }() //TODO: run all three in paraller //RunWebSocketServer - //RunHTTPServer + httpOpts := servers.TestServerOptions{ + Port: 8083, + } + wg.Add(1) + go func() { + defer wg.Done() + servers.RunHTTPServer(ctx, &wg, httpOpts) + }() // Wait for the server to start time.Sleep(2 * time.Second) diff --git a/integration_tests/commands/tests/parsers/http.go b/integration_tests/commands/tests/parsers/http.go index 3ece6e2d4..23853c6aa 100644 --- a/integration_tests/commands/tests/parsers/http.go +++ b/integration_tests/commands/tests/parsers/http.go @@ -1 +1,46 @@ package parsers + +import ( + "strings" + + "github.com/dicedb/dice/integration_tests/commands/tests/servers" +) + +func ParseResponse(response interface{}) interface{} { + // convert the output to the int64 if it is float64 + switch response.(type) { + case float64: + return int64(response.(float64)) + case nil: + return "(nil)" + default: + return response + } +} + +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/servers/http.go b/integration_tests/commands/tests/servers/http.go index 84c4cc049..9eecd553e 100644 --- a/integration_tests/commands/tests/servers/http.go +++ b/integration_tests/commands/tests/servers/http.go @@ -1 +1,145 @@ 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 CommandExecutor interface { + FireCommand(cmd string) interface{} + Name() string +} + +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 server on 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/set.go b/integration_tests/commands/tests/set.go index 7560703f2..70f93c2c9 100644 --- a/integration_tests/commands/tests/set.go +++ b/integration_tests/commands/tests/set.go @@ -7,7 +7,7 @@ import ( var expiryTime = strconv.FormatInt(time.Now().Add(1*time.Minute).UnixMilli(), 10) -var testCases = []Meta{ +var setTestCases = []Meta{ { Name: "Set and Get Simple Value", Input: []string{"SET k v", "GET k"}, @@ -125,5 +125,5 @@ var testCases = []Meta{ } func init() { - RegisterTests(testCases) + RegisterTests(setTestCases) }