Merge pull request #403 from tidwall/JSON-Output-for-INFO-and-CLIENT-commands

JSON Output for INFO and CLIENT
This commit is contained in:
Josh Baker 2019-01-16 12:09:13 -07:00 committed by GitHub
commit 8d25630d5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 163 additions and 3 deletions

View File

@ -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
}

View File

@ -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 {

69
tests/client_test.go Normal file
View File

@ -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
}

35
tests/stats_test.go Normal file
View File

@ -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
}

View File

@ -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)) {