diff --git a/internal/server/client.go b/internal/server/client.go index 3cb38d7c..87221feb 100644 --- a/internal/server/client.go +++ b/internal/server/client.go @@ -1,6 +1,7 @@ package server import ( + "encoding/json" "errors" "fmt" "io" @@ -90,7 +91,31 @@ func (c *Server) cmdClient(msg *Message, client *Client) (resp.Value, error) { } switch msg.OutputType { case JSON: - return resp.StringValue(`{"ok":true,"list":` + jsonString(string(buf)) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil + // Create a map of all key/value info fields + var cmap []map[string]interface{} + clients := strings.Split(string(buf), "\n") + for _, client := range clients { + client = strings.TrimSpace(client) + m := make(map[string]interface{}) + var hasFields bool + for _, kv := range strings.Split(client, " ") { + kv = strings.TrimSpace(kv) + if split := strings.SplitN(kv, "=", 2); len(split) == 2 { + hasFields = true + m[split[0]] = tryParseType(split[1]) + } + } + if hasFields { + cmap = append(cmap, m) + } + } + + // Marshal the map and use the output in the JSON response + data, err := json.Marshal(cmap) + if err != nil { + return NOMessage, err + } + return resp.StringValue(`{"ok":true,"list":` + string(data) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil case RESP: return resp.BytesValue(buf), nil } diff --git a/internal/server/stats.go b/internal/server/stats.go index a34240a2..aab2cc45 100644 --- a/internal/server/stats.go +++ b/internal/server/stats.go @@ -7,6 +7,7 @@ import ( "os" "runtime" "sort" + "strconv" "strings" "time" @@ -414,7 +415,19 @@ func (c *Server) cmdInfo(msg *Message) (res resp.Value, err error) { switch msg.OutputType { case JSON: - data, err := json.Marshal(w.String()) + // Create a map of all key/value info fields + m := make(map[string]interface{}) + for _, kv := range strings.Split(w.String(), "\r\n") { + kv = strings.TrimSpace(kv) + if !strings.HasPrefix(kv, "#") { + if split := strings.SplitN(kv, ":", 2); len(split) == 2 { + m[split[0]] = tryParseType(split[1]) + } + } + } + + // Marshal the map and use the output in the JSON response + data, err := json.Marshal(m) if err != nil { return NOMessage, err } @@ -422,9 +435,25 @@ func (c *Server) cmdInfo(msg *Message) (res resp.Value, err error) { case RESP: res = resp.BytesValue(w.Bytes()) } - return res, nil } + +// tryParseType attempts to parse the passed string as an integer, float64 and +// a bool returning any successful parsed values. It returns the passed string +// if all tries fail +func tryParseType(str string) interface{} { + if v, err := strconv.ParseInt(str, 10, 64); err == nil { + return v + } + if v, err := strconv.ParseFloat(str, 64); err == nil { + return v + } + if v, err := strconv.ParseBool(str); err == nil { + return v + } + return str +} + func respValuesSimpleMap(m map[string]interface{}) []resp.Value { var keys []string for key := range m { diff --git a/tests/client_test.go b/tests/client_test.go new file mode 100644 index 00000000..99fd03ce --- /dev/null +++ b/tests/client_test.go @@ -0,0 +1,69 @@ +package tests + +import ( + "errors" + "fmt" + "testing" + + "github.com/gomodule/redigo/redis" + "github.com/tidwall/gjson" +) + +func subTestClient(t *testing.T, mc *mockServer) { + runStep(t, mc, "valid json", client_valid_json_test) + runStep(t, mc, "valid client count", info_valid_client_count_test) +} + +func client_valid_json_test(mc *mockServer) error { + if _, err := mc.Do("OUTPUT", "JSON"); err != nil { + return err + } + res, err := mc.Do("CLIENT", "list") + if err != nil { + return err + } + bres, ok := res.([]byte) + if !ok { + return errors.New("Failed to type assert CLIENT response") + } + sres := string(bres) + if !gjson.Valid(sres) { + return errors.New("CLIENT response was invalid") + } + info := gjson.Get(sres, "list").String() + if !gjson.Valid(info) { + return errors.New("CLIENT.list response was invalid") + } + return nil +} + +func info_valid_client_count_test(mc *mockServer) error { + numConns := 20 + var conns []redis.Conn + for i := 0; i <= numConns; i++ { + conn, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port)) + if err != nil { + return err + } + conns = append(conns, conn) + } + for i := range conns { + defer conns[i].Close() + } + if _, err := mc.Do("OUTPUT", "JSON"); err != nil { + return err + } + res, err := mc.Do("CLIENT", "list") + if err != nil { + return err + } + bres, ok := res.([]byte) + if !ok { + return errors.New("Failed to type assert CLIENT response") + } + sres := string(bres) + if len(gjson.Get(sres, "list").Array()) < numConns { + return errors.New("Invalid number of connections") + } + return nil +} diff --git a/tests/stats_test.go b/tests/stats_test.go new file mode 100644 index 00000000..7cc466ae --- /dev/null +++ b/tests/stats_test.go @@ -0,0 +1,35 @@ +package tests + +import ( + "errors" + "testing" + + "github.com/tidwall/gjson" +) + +func subTestInfo(t *testing.T, mc *mockServer) { + runStep(t, mc, "valid json", info_valid_json_test) +} + +func info_valid_json_test(mc *mockServer) error { + if _, err := mc.Do("OUTPUT", "JSON"); err != nil { + return err + } + res, err := mc.Do("INFO") + if err != nil { + return err + } + bres, ok := res.([]byte) + if !ok { + return errors.New("Failed to type assert INFO response") + } + sres := string(bres) + if !gjson.Valid(sres) { + return errors.New("INFO response was invalid") + } + info := gjson.Get(sres, "info").String() + if !gjson.Valid(info) { + return errors.New("INFO.info response was invalid") + } + return nil +} diff --git a/tests/tests_test.go b/tests/tests_test.go index cfe198f3..4de3d18a 100644 --- a/tests/tests_test.go +++ b/tests/tests_test.go @@ -44,6 +44,8 @@ func TestAll(t *testing.T) { runSubTest(t, "search", mc, subTestSearch) runSubTest(t, "fence", mc, subTestFence) runSubTest(t, "scripts", mc, subTestScripts) + runSubTest(t, "info", mc, subTestInfo) + runSubTest(t, "client", mc, subTestClient) } func runSubTest(t *testing.T, name string, mc *mockServer, test func(t *testing.T, mc *mockServer)) {