From 0740ca13cd7eebc41826808ee2eeb8cfbe6e7dae Mon Sep 17 00:00:00 2001 From: Steven Wolfe Date: Mon, 14 Jan 2019 15:29:29 -0700 Subject: [PATCH 1/2] JSON Output for INFO and CLIENT --- internal/server/client.go | 18 +++++++++++++++++- internal/server/stats.go | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/internal/server/client.go b/internal/server/client.go index 3cb38d7c..a60d66e5 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,22 @@ 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 + m := make(map[string]interface{}) + res := strings.TrimSpace(string(buf)) + for _, kv := range strings.Split(res, " ") { + kv = strings.TrimSpace(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 + } + 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 { From ffd8ca915cbb579b413f1b2425608bd74f4e92c1 Mon Sep 17 00:00:00 2001 From: Steven Wolfe Date: Tue, 15 Jan 2019 11:08:19 -0700 Subject: [PATCH 2/2] Testing for valid INFO and CLIENT Json output --- internal/server/client.go | 23 +++++++++---- tests/client_test.go | 69 +++++++++++++++++++++++++++++++++++++++ tests/stats_test.go | 35 ++++++++++++++++++++ tests/tests_test.go | 2 ++ 4 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 tests/client_test.go create mode 100644 tests/stats_test.go diff --git a/internal/server/client.go b/internal/server/client.go index a60d66e5..87221feb 100644 --- a/internal/server/client.go +++ b/internal/server/client.go @@ -92,17 +92,26 @@ func (c *Server) cmdClient(msg *Message, client *Client) (resp.Value, error) { switch msg.OutputType { case JSON: // Create a map of all key/value info fields - m := make(map[string]interface{}) - res := strings.TrimSpace(string(buf)) - for _, kv := range strings.Split(res, " ") { - kv = strings.TrimSpace(kv) - if split := strings.SplitN(kv, "=", 2); len(split) == 2 { - m[split[0]] = tryParseType(split[1]) + 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(m) + data, err := json.Marshal(cmap) if err != nil { return NOMessage, err } 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)) {