sq/cli/output/jsonw/internal/internal_test.go

498 lines
17 KiB
Go
Raw Normal View History

2020-08-06 20:58:47 +03:00
package internal_test
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/require"
stdjson "encoding/json"
"github.com/neilotoole/sq/cli/output"
"github.com/neilotoole/sq/cli/output/jsonw/internal"
jcolorenc "github.com/neilotoole/sq/cli/output/jsonw/internal/jcolorenc"
)
// Encoder encapsulates the methods of a JSON encoder.
type Encoder interface {
Encode(v interface{}) error
SetEscapeHTML(on bool)
SetIndent(prefix, indent string)
}
var _ Encoder = (*jcolorenc.Encoder)(nil)
func TestEncode(t *testing.T) {
testCases := []struct {
name string
pretty bool
color bool
sortMap bool
v interface{}
want string
}{
{name: "nil", pretty: true, v: nil, want: "null\n"},
{name: "slice_empty", pretty: true, v: []int{}, want: "[]\n"},
{name: "slice_1_pretty", pretty: true, v: []interface{}{1}, want: "[\n 1\n]\n"},
{name: "slice_1_no_pretty", v: []interface{}{1}, want: "[1]\n"},
{name: "slice_2_pretty", pretty: true, v: []interface{}{1, true}, want: "[\n 1,\n true\n]\n"},
{name: "slice_2_no_pretty", v: []interface{}{1, true}, want: "[1,true]\n"},
{name: "map_int_empty", pretty: true, v: map[string]int{}, want: "{}\n"},
{name: "map_interface_empty", pretty: true, v: map[string]interface{}{}, want: "{}\n"},
{name: "map_interface_empty_sorted", pretty: true, sortMap: true, v: map[string]interface{}{}, want: "{}\n"},
{name: "map_1_pretty", pretty: true, sortMap: true, v: map[string]interface{}{"one": 1}, want: "{\n \"one\": 1\n}\n"},
{name: "map_1_no_pretty", sortMap: true, v: map[string]interface{}{"one": 1}, want: "{\"one\":1}\n"},
{name: "map_2_pretty", pretty: true, sortMap: true, v: map[string]interface{}{"one": 1, "two": 2}, want: "{\n \"one\": 1,\n \"two\": 2\n}\n"},
{name: "map_2_no_pretty", sortMap: true, v: map[string]interface{}{"one": 1, "two": 2}, want: "{\"one\":1,\"two\":2}\n"},
{name: "tinystruct", pretty: true, v: TinyStruct{FBool: true}, want: "{\n \"f_bool\": true\n}\n"},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = tc.pretty
fm.EnableColor(tc.color)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetSortMapKeys(tc.sortMap)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", fm.Indent)
}
require.NoError(t, enc.Encode(tc.v))
require.True(t, stdjson.Valid(buf.Bytes()))
require.Equal(t, tc.want, buf.String())
})
}
}
func TestEncode_Slice(t *testing.T) {
testCases := []struct {
name string
pretty bool
color bool
v []interface{}
want string
}{
{name: "nil", pretty: true, v: nil, want: "null\n"},
{name: "empty", pretty: true, v: []interface{}{}, want: "[]\n"},
{name: "one", pretty: true, v: []interface{}{1}, want: "[\n 1\n]\n"},
{name: "two", pretty: true, v: []interface{}{1, true}, want: "[\n 1,\n true\n]\n"},
{name: "three", pretty: true, v: []interface{}{1, true, "hello"}, want: "[\n 1,\n true,\n \"hello\"\n]\n"},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = tc.pretty
fm.EnableColor(tc.color)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", " ")
}
require.NoError(t, enc.Encode(tc.v))
require.True(t, stdjson.Valid(buf.Bytes()))
require.Equal(t, tc.want, buf.String())
})
}
}
func TestEncode_SmallStruct(t *testing.T) {
v := SmallStruct{
FInt: 7,
FSlice: []interface{}{64, true},
FMap: map[string]interface{}{
"m_float64": 64.64,
"m_string": "hello",
},
FTinyStruct: TinyStruct{FBool: true},
FString: "hello",
}
testCases := []struct {
pretty bool
color bool
want string
}{
{pretty: false, color: false, want: "{\"f_int\":7,\"f_slice\":[64,true],\"f_map\":{\"m_float64\":64.64,\"m_string\":\"hello\"},\"f_tinystruct\":{\"f_bool\":true},\"f_string\":\"hello\"}\n"},
{pretty: true, color: false, want: "{\n \"f_int\": 7,\n \"f_slice\": [\n 64,\n true\n ],\n \"f_map\": {\n \"m_float64\": 64.64,\n \"m_string\": \"hello\"\n },\n \"f_tinystruct\": {\n \"f_bool\": true\n },\n \"f_string\": \"hello\"\n}\n"},
}
for _, tc := range testCases {
tc := tc
t.Run(fmt.Sprintf("pretty_%v__color_%v", tc.pretty, tc.color), func(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = tc.pretty
fm.EnableColor(tc.color)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetSortMapKeys(true)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", " ")
}
require.NoError(t, enc.Encode(v))
require.True(t, stdjson.Valid(buf.Bytes()))
require.Equal(t, tc.want, buf.String())
})
}
}
func TestEncode_Map_Nested(t *testing.T) {
v := map[string]interface{}{
"m_bool1": true,
"m_nest1": map[string]interface{}{
"m_nest1_bool": true,
"m_nest2": map[string]interface{}{
"m_nest2_bool": true,
"m_nest3": map[string]interface{}{
"m_nest3_bool": true,
},
},
},
"m_string1": "hello",
}
testCases := []struct {
pretty bool
color bool
want string
}{
{pretty: false, want: "{\"m_bool1\":true,\"m_nest1\":{\"m_nest1_bool\":true,\"m_nest2\":{\"m_nest2_bool\":true,\"m_nest3\":{\"m_nest3_bool\":true}}},\"m_string1\":\"hello\"}\n"},
{pretty: true, want: "{\n \"m_bool1\": true,\n \"m_nest1\": {\n \"m_nest1_bool\": true,\n \"m_nest2\": {\n \"m_nest2_bool\": true,\n \"m_nest3\": {\n \"m_nest3_bool\": true\n }\n }\n },\n \"m_string1\": \"hello\"\n}\n"},
}
for _, tc := range testCases {
tc := tc
t.Run(fmt.Sprintf("pretty_%v__color_%v", tc.pretty, tc.color), func(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = tc.pretty
fm.EnableColor(tc.color)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetSortMapKeys(true)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", " ")
}
require.NoError(t, enc.Encode(v))
require.True(t, stdjson.Valid(buf.Bytes()))
require.Equal(t, tc.want, buf.String())
})
}
}
// TestEncode_Map_StringNotInterface tests maps with a string key
// but the value type is not interface{}.
// For example, map[string]bool. This test is necessary because the
// encoder has a fast path for map[string]interface{}
func TestEncode_Map_StringNotInterface(t *testing.T) {
testCases := []struct {
pretty bool
color bool
sortMap bool
v map[string]bool
want string
}{
{pretty: false, sortMap: true, v: map[string]bool{}, want: "{}\n"},
{pretty: false, sortMap: false, v: map[string]bool{}, want: "{}\n"},
{pretty: true, sortMap: true, v: map[string]bool{}, want: "{}\n"},
{pretty: true, sortMap: false, v: map[string]bool{}, want: "{}\n"},
{pretty: false, sortMap: true, v: map[string]bool{"one": true}, want: "{\"one\":true}\n"},
{pretty: false, sortMap: false, v: map[string]bool{"one": true}, want: "{\"one\":true}\n"},
{pretty: false, sortMap: true, v: map[string]bool{"one": true, "two": false}, want: "{\"one\":true,\"two\":false}\n"},
{pretty: true, sortMap: true, v: map[string]bool{"one": true, "two": false}, want: "{\n \"one\": true,\n \"two\": false\n}\n"},
}
for _, tc := range testCases {
tc := tc
t.Run(fmt.Sprintf("size_%d__pretty_%v__color_%v", len(tc.v), tc.pretty, tc.color), func(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = tc.pretty
fm.EnableColor(tc.color)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetSortMapKeys(tc.sortMap)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", fm.Indent)
}
require.NoError(t, enc.Encode(tc.v))
require.True(t, stdjson.Valid(buf.Bytes()))
require.Equal(t, tc.want, buf.String())
})
}
}
func TestEncode_RawMessage(t *testing.T) {
type RawStruct struct {
FString string `json:"f_string"`
FRaw jcolorenc.RawMessage `json:"f_raw"`
}
raw := jcolorenc.RawMessage(`{"one":1,"two":2}`)
testCases := []struct {
name string
pretty bool
color bool
v interface{}
want string
}{
{name: "empty", pretty: false, v: jcolorenc.RawMessage(`{}`), want: "{}\n"},
{name: "no_pretty", pretty: false, v: raw, want: "{\"one\":1,\"two\":2}\n"},
{name: "pretty", pretty: true, v: raw, want: "{\n \"one\": 1,\n \"two\": 2\n}\n"},
{name: "pretty_struct", pretty: true, v: RawStruct{FString: "hello", FRaw: raw}, want: "{\n \"f_string\": \"hello\",\n \"f_raw\": {\n \"one\": 1,\n \"two\": 2\n }\n}\n"},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = tc.pretty
fm.EnableColor(tc.color)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetSortMapKeys(true)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", fm.Indent)
}
err := enc.Encode(tc.v)
require.NoError(t, err)
require.True(t, stdjson.Valid(buf.Bytes()))
require.Equal(t, tc.want, buf.String())
})
}
}
// TestEncode_Map_StringNotInterface tests map[string]json.RawMessage.
// This test is necessary because the encoder has a fast path
// for map[string]interface{}
func TestEncode_Map_StringRawMessage(t *testing.T) {
raw := jcolorenc.RawMessage(`{"one":1,"two":2}`)
testCases := []struct {
pretty bool
color bool
sortMap bool
v map[string]jcolorenc.RawMessage
want string
}{
{pretty: false, sortMap: true, v: map[string]jcolorenc.RawMessage{}, want: "{}\n"},
{pretty: false, sortMap: false, v: map[string]jcolorenc.RawMessage{}, want: "{}\n"},
{pretty: true, sortMap: true, v: map[string]jcolorenc.RawMessage{}, want: "{}\n"},
{pretty: true, sortMap: false, v: map[string]jcolorenc.RawMessage{}, want: "{}\n"},
{pretty: false, sortMap: true, v: map[string]jcolorenc.RawMessage{"msg1": raw, "msg2": raw}, want: "{\"msg1\":{\"one\":1,\"two\":2},\"msg2\":{\"one\":1,\"two\":2}}\n"},
{pretty: true, sortMap: true, v: map[string]jcolorenc.RawMessage{"msg1": raw, "msg2": raw}, want: "{\n \"msg1\": {\n \"one\": 1,\n \"two\": 2\n },\n \"msg2\": {\n \"one\": 1,\n \"two\": 2\n }\n}\n"},
{pretty: true, sortMap: false, v: map[string]jcolorenc.RawMessage{"msg1": raw}, want: "{\n \"msg1\": {\n \"one\": 1,\n \"two\": 2\n }\n}\n"},
}
for _, tc := range testCases {
tc := tc
name := fmt.Sprintf("size_%d__pretty_%v__color_%v__sort_%v", len(tc.v), tc.pretty, tc.color, tc.sortMap)
t.Run(name, func(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = tc.pretty
fm.EnableColor(tc.color)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetSortMapKeys(tc.sortMap)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", fm.Indent)
}
require.NoError(t, enc.Encode(tc.v))
require.True(t, stdjson.Valid(buf.Bytes()))
require.Equal(t, tc.want, buf.String())
})
}
}
func TestEncode_BigStruct(t *testing.T) {
v := newBigStruct()
testCases := []struct {
pretty bool
color bool
want string
}{
{pretty: false, want: "{\"f_int\":-7,\"f_int8\":-8,\"f_int16\":-16,\"f_int32\":-32,\"f_int64\":-64,\"f_uint\":7,\"f_uint8\":8,\"f_uint16\":16,\"f_uint32\":32,\"f_uint64\":64,\"f_float32\":32.32,\"f_float64\":64.64,\"f_bool\":true,\"f_bytes\":\"aGVsbG8=\",\"f_nil\":null,\"f_string\":\"hello\",\"f_map\":{\"m_bool\":true,\"m_int64\":64,\"m_nil\":null,\"m_smallstruct\":{\"f_int\":7,\"f_slice\":[64,true],\"f_map\":{\"m_float64\":64.64,\"m_string\":\"hello\"},\"f_tinystruct\":{\"f_bool\":true},\"f_string\":\"hello\"},\"m_string\":\"hello\"},\"f_smallstruct\":{\"f_int\":7,\"f_slice\":[64,true],\"f_map\":{\"m_float64\":64.64,\"m_string\":\"hello\"},\"f_tinystruct\":{\"f_bool\":true},\"f_string\":\"hello\"},\"f_interface\":\"hello\",\"f_interfaces\":[64,\"hello\",true]}\n"},
{pretty: true, want: "{\n \"f_int\": -7,\n \"f_int8\": -8,\n \"f_int16\": -16,\n \"f_int32\": -32,\n \"f_int64\": -64,\n \"f_uint\": 7,\n \"f_uint8\": 8,\n \"f_uint16\": 16,\n \"f_uint32\": 32,\n \"f_uint64\": 64,\n \"f_float32\": 32.32,\n \"f_float64\": 64.64,\n \"f_bool\": true,\n \"f_bytes\": \"aGVsbG8=\",\n \"f_nil\": null,\n \"f_string\": \"hello\",\n \"f_map\": {\n \"m_bool\": true,\n \"m_int64\": 64,\n \"m_nil\": null,\n \"m_smallstruct\": {\n \"f_int\": 7,\n \"f_slice\": [\n 64,\n true\n ],\n \"f_map\": {\n \"m_float64\": 64.64,\n \"m_string\": \"hello\"\n },\n \"f_tinystruct\": {\n \"f_bool\": true\n },\n \"f_string\": \"hello\"\n },\n \"m_string\": \"hello\"\n },\n \"f_smallstruct\": {\n \"f_int\": 7,\n \"f_slice\": [\n 64,\n true\n ],\n \"f_map\": {\n \"m_float64\": 64.64,\n \"m_string\": \"hello\"\n },\n \"f_tinystruct\": {\n \"f_bool\": true\n },\n \"f_string\": \"hello\"\n },\n \"f_interface\": \"hello\",\n \"f_interfaces\": [\n 64,\n \"hello\",\n true\n ]\n}\n"},
}
for _, tc := range testCases {
tc := tc
t.Run(fmt.Sprintf("pretty_%v__color_%v", tc.pretty, tc.color), func(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = tc.pretty
fm.EnableColor(tc.color)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetSortMapKeys(true)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", " ")
}
require.NoError(t, enc.Encode(v))
require.True(t, stdjson.Valid(buf.Bytes()))
require.Equal(t, tc.want, buf.String())
})
}
}
// TestEncode_Map_Not_StringInterface tests map encoding where
// the map is not map[string]interface{} (for which the encoder
// has a fast path).
//
// NOTE: Currently the encoder is broken wrt colors enabled
// for non-string map keys. It's possible we don't actually need
// to address this for sq purposes.
func TestEncode_Map_Not_StringInterface(t *testing.T) {
fm := output.NewFormatting()
fm.Pretty = true
fm.EnableColor(true)
buf := &bytes.Buffer{}
enc := jcolorenc.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetSortMapKeys(true)
enc.SetColors(internal.NewColors(fm))
if fm.Pretty {
enc.SetIndent("", " ")
}
v := map[int32]string{
0: "zero",
1: "one",
2: "two",
}
require.NoError(t, enc.Encode(v))
require.False(t, stdjson.Valid(buf.Bytes()),
"expected to be invalid JSON because the encoder currently doesn't handle maps with non-string keys")
}
// BigStruct is a big test struct.
type BigStruct struct {
FInt int `json:"f_int"`
FInt8 int8 `json:"f_int8"`
FInt16 int16 `json:"f_int16"`
FInt32 int32 `json:"f_int32"`
FInt64 int64 `json:"f_int64"`
FUint uint `json:"f_uint"`
FUint8 uint8 `json:"f_uint8"`
FUint16 uint16 `json:"f_uint16"`
FUint32 uint32 `json:"f_uint32"`
FUint64 uint64 `json:"f_uint64"`
FFloat32 float32 `json:"f_float32"`
FFloat64 float64 `json:"f_float64"`
FBool bool `json:"f_bool"`
FBytes []byte `json:"f_bytes"`
FNil interface{} `json:"f_nil"`
FString string `json:"f_string"`
FMap map[string]interface{} `json:"f_map"`
FSmallStruct SmallStruct `json:"f_smallstruct"`
FInterface interface{} `json:"f_interface"`
FInterfaces []interface{} `json:"f_interfaces"`
}
// SmallStruct is a small test struct.
type SmallStruct struct {
FInt int `json:"f_int"`
FSlice []interface{} `json:"f_slice"`
FMap map[string]interface{} `json:"f_map"`
FTinyStruct TinyStruct `json:"f_tinystruct"`
FString string `json:"f_string"`
}
// Tiny Struct is a tiny test struct.
type TinyStruct struct {
FBool bool `json:"f_bool"`
}
func newBigStruct() BigStruct {
return BigStruct{
FInt: -7,
FInt8: -8,
FInt16: -16,
FInt32: -32,
FInt64: -64,
FUint: 7,
FUint8: 8,
FUint16: 16,
FUint32: 32,
FUint64: 64,
FFloat32: 32.32,
FFloat64: 64.64,
FBool: true,
FBytes: []byte("hello"),
FNil: nil,
FString: "hello",
FMap: map[string]interface{}{
"m_int64": int64(64),
"m_string": "hello",
"m_bool": true,
"m_nil": nil,
"m_smallstruct": newSmallStruct(),
},
FSmallStruct: newSmallStruct(),
FInterface: interface{}("hello"),
FInterfaces: []interface{}{int64(64), "hello", true},
}
}
func newSmallStruct() SmallStruct {
return SmallStruct{
FInt: 7,
FSlice: []interface{}{64, true},
FMap: map[string]interface{}{
"m_float64": 64.64,
"m_string": "hello",
},
FTinyStruct: TinyStruct{FBool: true},
FString: "hello",
}
}