blob: 8572c734bdb67f718cfd373072161752ce2182f8 [file] [log] [blame]
package yaml
import (
"encoding/json"
"fmt"
"math"
"reflect"
"sort"
"strconv"
"testing"
"github.com/davecgh/go-spew/spew"
yaml "gopkg.in/yaml.v2"
)
type MarshalTest struct {
A string
B int64
// Would like to test float64, but it's not supported in go-yaml.
// (See https://github.com/go-yaml/yaml/issues/83.)
C float32
}
func TestMarshal(t *testing.T) {
f32String := strconv.FormatFloat(math.MaxFloat32, 'g', -1, 32)
s := MarshalTest{"a", math.MaxInt64, math.MaxFloat32}
e := []byte(fmt.Sprintf("A: a\nB: %d\nC: %s\n", math.MaxInt64, f32String))
y, err := Marshal(s)
if err != nil {
t.Errorf("error marshaling YAML: %v", err)
}
if !reflect.DeepEqual(y, e) {
t.Errorf("marshal YAML was unsuccessful, expected: %#v, got: %#v",
string(e), string(y))
}
}
type UnmarshalString struct {
A string
True string
}
type UnmarshalStringMap struct {
A map[string]string
}
type UnmarshalNestedString struct {
A NestedString
}
type NestedString struct {
A string
}
type UnmarshalSlice struct {
A []NestedSlice
}
type NestedSlice struct {
B string
C *string
}
func TestUnmarshal(t *testing.T) {
y := []byte("a: 1")
s1 := UnmarshalString{}
e1 := UnmarshalString{A: "1"}
unmarshal(t, y, &s1, &e1)
y = []byte("a: true")
s1 = UnmarshalString{}
e1 = UnmarshalString{A: "true"}
unmarshal(t, y, &s1, &e1)
y = []byte("true: 1")
s1 = UnmarshalString{}
e1 = UnmarshalString{True: "1"}
unmarshal(t, y, &s1, &e1)
y = []byte("a:\n a: 1")
s2 := UnmarshalNestedString{}
e2 := UnmarshalNestedString{NestedString{"1"}}
unmarshal(t, y, &s2, &e2)
y = []byte("a:\n - b: abc\n c: def\n - b: 123\n c: 456\n")
s3 := UnmarshalSlice{}
e3 := UnmarshalSlice{[]NestedSlice{NestedSlice{"abc", strPtr("def")}, NestedSlice{"123", strPtr("456")}}}
unmarshal(t, y, &s3, &e3)
y = []byte("a:\n b: 1")
s4 := UnmarshalStringMap{}
e4 := UnmarshalStringMap{map[string]string{"b": "1"}}
unmarshal(t, y, &s4, &e4)
y = []byte(`
a:
name: TestA
b:
name: TestB
`)
type NamedThing struct {
Name string `json:"name"`
}
s5 := map[string]*NamedThing{}
e5 := map[string]*NamedThing{
"a": &NamedThing{Name: "TestA"},
"b": &NamedThing{Name: "TestB"},
}
unmarshal(t, y, &s5, &e5)
}
func unmarshal(t *testing.T, y []byte, s, e interface{}, opts ...JSONOpt) {
err := Unmarshal(y, s, opts...)
if err != nil {
t.Errorf("error unmarshaling YAML: %v", err)
}
if !reflect.DeepEqual(s, e) {
t.Errorf("unmarshal YAML was unsuccessful, expected: %+#v, got: %+#v",
e, s)
}
}
func TestUnmarshalStrict(t *testing.T) {
y := []byte("a: 1")
s1 := UnmarshalString{}
e1 := UnmarshalString{A: "1"}
unmarshalStrict(t, y, &s1, &e1)
y = []byte("a: true")
s1 = UnmarshalString{}
e1 = UnmarshalString{A: "true"}
unmarshalStrict(t, y, &s1, &e1)
y = []byte("true: 1")
s1 = UnmarshalString{}
e1 = UnmarshalString{True: "1"}
unmarshalStrict(t, y, &s1, &e1)
y = []byte("a:\n a: 1")
s2 := UnmarshalNestedString{}
e2 := UnmarshalNestedString{NestedString{"1"}}
unmarshalStrict(t, y, &s2, &e2)
y = []byte("a:\n - b: abc\n c: def\n - b: 123\n c: 456\n")
s3 := UnmarshalSlice{}
e3 := UnmarshalSlice{[]NestedSlice{NestedSlice{"abc", strPtr("def")}, NestedSlice{"123", strPtr("456")}}}
unmarshalStrict(t, y, &s3, &e3)
y = []byte("a:\n b: 1")
s4 := UnmarshalStringMap{}
e4 := UnmarshalStringMap{map[string]string{"b": "1"}}
unmarshalStrict(t, y, &s4, &e4)
y = []byte(`
a:
name: TestA
b:
name: TestB
`)
type NamedThing struct {
Name string `json:"name"`
}
s5 := map[string]*NamedThing{}
e5 := map[string]*NamedThing{
"a": &NamedThing{Name: "TestA"},
"b": &NamedThing{Name: "TestB"},
}
unmarshal(t, y, &s5, &e5)
// When using not-so-strict unmarshal, we should
// be picking up the ID-1 as the value in the "id" field
y = []byte(`
a:
name: TestA
id: ID-A
id: ID-1
`)
type NamedThing2 struct {
Name string `json:"name"`
ID string `json:"id"`
}
s6 := map[string]*NamedThing2{}
e6 := map[string]*NamedThing2{
"a": {Name: "TestA", ID: "ID-1"},
}
unmarshal(t, y, &s6, &e6)
}
func TestUnmarshalStrictFails(t *testing.T) {
y := []byte("a: true\na: false")
s1 := UnmarshalString{}
unmarshalStrictFail(t, y, &s1)
y = []byte("a:\n - b: abc\n c: 32\n b: 123")
s2 := UnmarshalSlice{}
unmarshalStrictFail(t, y, &s2)
y = []byte("a:\n b: 1\n c: 3")
s3 := UnmarshalStringMap{}
unmarshalStrictFail(t, y, &s3)
type NamedThing struct {
Name string `json:"name"`
ID string `json:"id"`
}
// When using strict unmarshal, we should see
// the unmarshal fail if there are multiple keys
y = []byte(`
a:
name: TestA
id: ID-A
id: ID-1
`)
s4 := NamedThing{}
unmarshalStrictFail(t, y, &s4)
// Strict unmarshal should fail for unknown fields
y = []byte(`
name: TestB
id: ID-B
unknown: Some-Value
`)
s5 := NamedThing{}
unmarshalStrictFail(t, y, &s5)
}
func unmarshalStrict(t *testing.T, y []byte, s, e interface{}, opts ...JSONOpt) {
err := UnmarshalStrict(y, s, opts...)
if err != nil {
t.Errorf("error unmarshaling YAML: %v", err)
}
if !reflect.DeepEqual(s, e) {
t.Errorf("unmarshal YAML was unsuccessful, expected: %+#v, got: %+#v",
e, s)
}
}
func unmarshalStrictFail(t *testing.T, y []byte, s interface{}, opts ...JSONOpt) {
err := UnmarshalStrict(y, s, opts...)
if err == nil {
t.Errorf("error unmarshaling YAML: %v", err)
}
}
type Case struct {
input string
output string
// By default we test that reversing the output == input. But if there is a
// difference in the reversed output, you can optionally specify it here.
reverse *string
}
type RunType int
const (
RunTypeJSONToYAML RunType = iota
RunTypeYAMLToJSON
)
func TestJSONToYAML(t *testing.T) {
cases := []Case{
{
`{"t":"a"}`,
"t: a\n",
nil,
}, {
`{"t":null}`,
"t: null\n",
nil,
},
}
runCases(t, RunTypeJSONToYAML, cases)
}
func TestYAMLToJSON(t *testing.T) {
cases := []Case{
{
"t: a\n",
`{"t":"a"}`,
nil,
}, {
"t: \n",
`{"t":null}`,
strPtr("t: null\n"),
}, {
"t: null\n",
`{"t":null}`,
nil,
}, {
"1: a\n",
`{"1":"a"}`,
strPtr("\"1\": a\n"),
}, {
"1000000000000000000000000000000000000: a\n",
`{"1e+36":"a"}`,
strPtr("\"1e+36\": a\n"),
}, {
"1e+36: a\n",
`{"1e+36":"a"}`,
strPtr("\"1e+36\": a\n"),
}, {
"\"1e+36\": a\n",
`{"1e+36":"a"}`,
nil,
}, {
"\"1.2\": a\n",
`{"1.2":"a"}`,
nil,
}, {
"- t: a\n",
`[{"t":"a"}]`,
nil,
}, {
"- t: a\n" +
"- t:\n" +
" b: 1\n" +
" c: 2\n",
`[{"t":"a"},{"t":{"b":1,"c":2}}]`,
nil,
}, {
`[{t: a}, {t: {b: 1, c: 2}}]`,
`[{"t":"a"},{"t":{"b":1,"c":2}}]`,
strPtr("- t: a\n" +
"- t:\n" +
" b: 1\n" +
" c: 2\n"),
}, {
"- t: \n",
`[{"t":null}]`,
strPtr("- t: null\n"),
}, {
"- t: null\n",
`[{"t":null}]`,
nil,
},
}
// Cases that should produce errors.
_ = []Case{
{
"~: a",
`{"null":"a"}`,
nil,
}, {
"a: !!binary gIGC\n",
"{\"a\":\"\x80\x81\x82\"}",
nil,
},
}
runCases(t, RunTypeYAMLToJSON, cases)
}
func runCases(t *testing.T, runType RunType, cases []Case) {
var f func([]byte) ([]byte, error)
var invF func([]byte) ([]byte, error)
var msg string
var invMsg string
if runType == RunTypeJSONToYAML {
f = JSONToYAML
invF = YAMLToJSON
msg = "JSON to YAML"
invMsg = "YAML back to JSON"
} else {
f = YAMLToJSON
invF = JSONToYAML
msg = "YAML to JSON"
invMsg = "JSON back to YAML"
}
for _, c := range cases {
// Convert the string.
t.Logf("converting %s\n", c.input)
output, err := f([]byte(c.input))
if err != nil {
t.Errorf("Failed to convert %s, input: `%s`, err: %v", msg, c.input, err)
}
// Check it against the expected output.
if string(output) != c.output {
t.Errorf("Failed to convert %s, input: `%s`, expected `%s`, got `%s`",
msg, c.input, c.output, string(output))
}
// Set the string that we will compare the reversed output to.
reverse := c.input
// If a special reverse string was specified, use that instead.
if c.reverse != nil {
reverse = *c.reverse
}
// Reverse the output.
input, err := invF(output)
if err != nil {
t.Errorf("Failed to convert %s, input: `%s`, err: %v", invMsg, string(output), err)
}
// Check the reverse is equal to the input (or to *c.reverse).
if string(input) != reverse {
t.Errorf("Failed to convert %s, input: `%s`, expected `%s`, got `%s`",
invMsg, string(output), reverse, string(input))
}
}
}
// To be able to easily fill in the *Case.reverse string above.
func strPtr(s string) *string {
return &s
}
func TestYAMLToJSONStrict(t *testing.T) {
const data = `
foo: bar
foo: baz
`
if _, err := YAMLToJSON([]byte(data)); err != nil {
t.Error("expected YAMLtoJSON to pass on duplicate field names")
}
if _, err := YAMLToJSONStrict([]byte(data)); err == nil {
t.Error("expected YAMLtoJSONStrict to fail on duplicate field names")
}
}
func TestJSONObjectToYAMLObject(t *testing.T) {
const bigUint64 = ((uint64(1) << 63) + 500) / 1000 * 1000
intOrInt64 := func(i64 int64) interface{} {
if i := int(i64); i64 == int64(i) {
return i
}
return i64
}
tests := []struct {
name string
input map[string]interface{}
expected yaml.MapSlice
}{
{name: "nil", expected: yaml.MapSlice(nil)},
{name: "empty", input: map[string]interface{}{}, expected: yaml.MapSlice(nil)},
{
name: "values",
input: map[string]interface{}{
"nil slice": []interface{}(nil),
"nil map": map[string]interface{}(nil),
"empty slice": []interface{}{},
"empty map": map[string]interface{}{},
"bool": true,
"float64": float64(42.1),
"fractionless": float64(42),
"int": int(42),
"int64": int64(42),
"int64 big": float64(math.Pow(2, 62)),
"negative int64 big": -float64(math.Pow(2, 62)),
"map": map[string]interface{}{"foo": "bar"},
"slice": []interface{}{"foo", "bar"},
"string": string("foo"),
"uint64 big": bigUint64,
},
expected: yaml.MapSlice{
{Key: "nil slice"},
{Key: "nil map"},
{Key: "empty slice", Value: []interface{}{}},
{Key: "empty map", Value: yaml.MapSlice(nil)},
{Key: "bool", Value: true},
{Key: "float64", Value: float64(42.1)},
{Key: "fractionless", Value: int(42)},
{Key: "int", Value: int(42)},
{Key: "int64", Value: int(42)},
{Key: "int64 big", Value: intOrInt64(int64(1) << 62)},
{Key: "negative int64 big", Value: intOrInt64(-(1 << 62))},
{Key: "map", Value: yaml.MapSlice{{Key: "foo", Value: "bar"}}},
{Key: "slice", Value: []interface{}{"foo", "bar"}},
{Key: "string", Value: string("foo")},
{Key: "uint64 big", Value: bigUint64},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := JSONObjectToYAMLObject(tt.input)
sortMapSlicesInPlace(tt.expected)
sortMapSlicesInPlace(got)
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("jsonToYAML() = %v, want %v", spew.Sdump(got), spew.Sdump(tt.expected))
}
jsonBytes, err := json.Marshal(tt.input)
if err != nil {
t.Fatalf("unexpected json.Marshal error: %v", err)
}
var gotByRoundtrip yaml.MapSlice
if err := yaml.Unmarshal(jsonBytes, &gotByRoundtrip); err != nil {
t.Fatalf("unexpected yaml.Unmarshal error: %v", err)
}
// yaml.Unmarshal loses precision, it's rounding to the 4th last digit.
// Replicate this here in the test, but don't change the type.
for i := range got {
switch got[i].Key {
case "int64 big", "uint64 big", "negative int64 big":
switch v := got[i].Value.(type) {
case int64:
d := int64(500)
if v < 0 {
d = -500
}
got[i].Value = int64((v+d)/1000) * 1000
case uint64:
got[i].Value = uint64((v+500)/1000) * 1000
case int:
d := int(500)
if v < 0 {
d = -500
}
got[i].Value = int((v+d)/1000) * 1000
default:
t.Fatalf("unexpected type for key %s: %v:%T", got[i].Key, v, v)
}
}
}
if !reflect.DeepEqual(got, gotByRoundtrip) {
t.Errorf("yaml.Unmarshal(json.Marshal(tt.input)) = %v, want %v\njson: %s", spew.Sdump(gotByRoundtrip), spew.Sdump(got), string(jsonBytes))
}
})
}
}
func sortMapSlicesInPlace(x interface{}) {
switch x := x.(type) {
case []interface{}:
for i := range x {
sortMapSlicesInPlace(x[i])
}
case yaml.MapSlice:
sort.Slice(x, func(a, b int) bool {
return x[a].Key.(string) < x[b].Key.(string)
})
}
}