Skip to content

Commit

Permalink
added http test runner + parser
Browse files Browse the repository at this point in the history
  • Loading branch information
apoorvyadav1111 committed Dec 3, 2024
1 parent 0ccc0c7 commit 69df546
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 6 deletions.
44 changes: 43 additions & 1 deletion integration_tests/commands/tests/aggregator.go
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -22,3 +53,14 @@ func RegisterTests(tests []Meta) {
func GetAllTests() []Meta {
return allTests
}

func SwitchAsserts(t *testing.T, kind string, expected interface{}, actual interface{}) {

Check failure on line 57 in integration_tests/commands/tests/aggregator.go

View workflow job for this annotation

GitHub Actions / lint

paramTypeCombine: func(t *testing.T, kind string, expected interface{}, actual interface{}) could be replaced with func(t *testing.T, kind string, expected, actual interface{}) (gocritic)
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)
}
}
30 changes: 30 additions & 0 deletions integration_tests/commands/tests/get.go
Original file line number Diff line number Diff line change
@@ -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)
}
60 changes: 60 additions & 0 deletions integration_tests/commands/tests/http_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

}
}
13 changes: 10 additions & 3 deletions integration_tests/commands/tests/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
45 changes: 45 additions & 0 deletions integration_tests/commands/tests/parsers/http.go
Original file line number Diff line number Diff line change
@@ -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) {

Check failure on line 11 in integration_tests/commands/tests/parsers/http.go

View workflow job for this annotation

GitHub Actions / lint

typeSwitchVar: 1 case can benefit from type switch with assignment (gocritic)

Check failure on line 11 in integration_tests/commands/tests/parsers/http.go

View workflow job for this annotation

GitHub Actions / lint

S1034: assigning the result of this type assertion to a variable (switch response := response.(type)) could eliminate type assertions in switch cases (gosimple)
case float64:
return int64(response.(float64))

Check failure on line 13 in integration_tests/commands/tests/parsers/http.go

View workflow job for this annotation

GitHub Actions / lint

S1034(related information): could eliminate this type assertion (gosimple)
case nil:
return "(nil)"
default:
return response
}
}

func HttpCommandExecuter(exec *servers.HTTPCommandExecutor, cmd string) (interface{}, error) {

Check failure on line 21 in integration_tests/commands/tests/parsers/http.go

View workflow job for this annotation

GitHub Actions / lint

ST1003: func HttpCommandExecuter should be HTTPCommandExecuter (stylecheck)
// 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{

Check failure on line 37 in integration_tests/commands/tests/parsers/http.go

View workflow job for this annotation

GitHub Actions / lint

ST1003: var diceHttpCmd should be diceHTTPCmd (stylecheck)
Command: strings.ToLower(command),
Body: body,
}
res, err := exec.FireCommand(diceHttpCmd)
if err != nil {
return nil, err
}
return ParseResponse(res), nil
}
144 changes: 144 additions & 0 deletions integration_tests/commands/tests/servers/http.go
Original file line number Diff line number Diff line change
@@ -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)
}
}()
}
4 changes: 2 additions & 2 deletions integration_tests/commands/tests/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -125,5 +125,5 @@ var testCases = []Meta{
}

func init() {
RegisterTests(testCases)
RegisterTests(setTestCases)
}

0 comments on commit 69df546

Please sign in to comment.